English Русский 中文 Español 日本語 Português
preview
Entwicklung eines Replay-Systems — Marktsimulation (Teil 11): Die Geburt des SIMULATORS (I)

Entwicklung eines Replay-Systems — Marktsimulation (Teil 11): Die Geburt des SIMULATORS (I)

MetaTrader 5Beispiele | 20 November 2023, 09:33
228 0
Daniel Jose
Daniel Jose

Einführung

Bisher, einschließlich des vorherigen Artikels „Entwicklung eines Replay-Systems — Marktsimulation (Teil 10): Ausschließlich reale Daten für das Replay verwenden“, haben wir ausschließlich reale Daten verwendet, das heißt, wir haben tatsächlich gehandelte Tickets verwendet. Dies macht die Bewegungen präzise und einfach zu erstellen, da wir uns nicht um die Erfassung von Informationen kümmern müssen. Alles, was wir tun mussten, war, die gehandelten Tickets in 1-Minuten-Balken umzuwandeln, und die MetaTrader 5-Plattform erledigte den Rest für uns.

Allerdings stehen wir jetzt vor einer weiteren, noch schwierigeren Aufgabe.


Planung

Viele denken vielleicht, dass die Planung einfach ist, vor allem, weil es darum geht, Balken, die immer 1 Minute lang sein sollten (wir werden später erklären, warum), in Tickets umzuwandeln. Die Simulation ist jedoch viel komplexer, als es auf den ersten Blick scheint. Das Hauptproblem besteht darin, dass wir kein klares Verständnis des tatsächlichen Verhaltens der Tickets zur Erstellung eines 1-Minuten-Balkens haben. Wir haben nur den Balken und einige Informationen über ihn, aber wir wissen nicht, wie der Balken entstanden ist. Wir werden 1-Minuten-Balken verwenden, weil sie den geringsten Schwierigkeitsgrad aufweisen. Wenn wir eine komplexe Bewegung erzeugen können, die der echten sehr ähnlich ist, dann können wir etwas reproduzieren, das der echten Sache sehr nahe kommt.

Dieses Detail mag nicht so wichtig erscheinen, da wir in der Regel eine Zick-Zack-Bewegung auf dem Markt sehen. Unabhängig von der Komplexität der Bewegung läuft alles darauf hinaus, ein Zickzack zwischen den OHCL-Punkten zu erzeugen. Er beginnt am Anfang des Balkens und führt nicht weniger als 9 Bewegungen aus, um diesen inneren Zickzack zu erzeugen. Er endet immer mit dem Ende des Balkens und wiederholt den Vorgang beim nächsten Takt. Der MetaTrader 5 Strategietester verwendet dieselbe Logik. Weitere Einzelheiten finden Sie unter Reale und generierte Ticks: Algorithmischer Handel. Wir werden mit dieser Strategie beginnen. Obwohl es für unsere Zwecke nicht ideal ist, bietet es einen Ausgangspunkt für die Entwicklung geeigneterer Ansätze.

Ich behaupte, dass die Tester-Strategie für das Replay-/Simulationssystem nicht am besten geeignet ist, weil bei einem Handelsstrategie-Tester die Zeit nicht von größter Bedeutung ist. Das heißt, es ist nicht notwendig, einen 1-Minuten-Balken so zu erstellen und darzustellen, dass seine Länge tatsächlich 1 Minute beträgt. Es ist sogar noch bequemer, dass sie in der Realität nicht diesem Zeitpunkt entspricht. Wäre dies der Fall, würde das Testen einer Strategie unmöglich werden. Stellen Sie sich vor, Sie führen einen Test mit Balken durch, die sich über mehrere Tage oder sogar Jahre erstrecken, wenn jeder Balken eine andere tatsächliche Zeit darstellt. Dies wäre eine unmögliche Aufgabe. Für ein Replay-/Simulationssystem suchen wir jedoch nach einer anderen Dynamik. Wir wollen, dass ein 1-Minuten-Balken in 1-Minuten-Intervallen erstellt wird, und zwar so nah wie möglich daran.


Vorbereitung des Bodens

Wir konzentrieren uns ausschließlich auf den Code des Replay-/Simulationsdienstes. Es gibt keinen Grund, sich zu diesem Zeitpunkt über andere Aspekte Gedanken zu machen. Wir werden also damit beginnen, den Code der Klasse C_Replay zu ändern und versuchen, das, was wir bereits entwickelt und getestet haben, so weit wie möglich zu optimieren. Hier ist die erste Prozedur, die in der Klasse erscheint:

inline bool CheckFileIsBar(int &file, const string szFileName)
                        {
                                string  szInfo = "";
                                bool    bRet;
                                
                                for (int c0 = 0; (c0 < 9) && (!FileIsEnding(file)); c0++) szInfo += FileReadString(file);
                                if ((bRet = (szInfo == def_Header_Bar)) == false)
                                {
                                        Print("File ", szFileName, ".csv is not a file with bars.");
                                        FileClose(file);
                                }
                                
                                return bRet;
                        }


Ziel ist es, die Tests, die feststellen, ob die angegebene Datei eine Datei mit Vorschaubalken ist oder nicht, aus der Funktion zum Lesen von Balken zu entfernen. Dies ist notwendig, um eine Wiederholung des Codes zu vermeiden, wenn es wichtig ist, denselben Satz zu verwenden, um festzustellen, ob die Datei mit den Balken unsere ist. In diesem Fall werden diese Balken nicht als Vorschaubalken verwendet. Sie werden in simulierte Tickets zur Verwendung im Handelssystem umgewandelt. Auf dieser Grundlage führen wir eine weitere Funktion ein:

inline void FileReadBars(int &file, MqlRates &rate[])
                        {
                                rate[0].time = StringToTime(FileReadString(file) + " " + FileReadString(file));
                                rate[0].open = StringToDouble(FileReadString(file));
                                rate[0].high = StringToDouble(FileReadString(file));
                                rate[0].low = StringToDouble(FileReadString(file));
                                rate[0].close = StringToDouble(FileReadString(file));
                                rate[0].tick_volume = StringToInteger(FileReadString(file));
                                rate[0].real_volume = StringToInteger(FileReadString(file));
                                rate[0].spread = (int) StringToInteger(FileReadString(file));
                        }


Es liest die Daten zeilenweise aus den Balken in der angegebenen Datei. Ich denke, Sie werden keine Schwierigkeiten haben, diesen Code zu verstehen. Als Fortsetzung dieser Vorbereitungsphase gibt es eine weitere Funktion:

inline bool OpenFileBars(int &file, const string szFileName)
                        {
                                if ((file = FileOpen("Market Replay\\Bars\\" + szFileName + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
                                {
                                        if (!CheckFileIsBar(file, szFileName))
                                                return false;
                                        return true;
                                }
                                Print("Falha ao acessar ", szFileName, ".csv de barras.");
                                
                                return false;
                        }


Wir haben unser System nun vollständig zentralisiert, um einen Standardzugang zu den Tafeln zu ermöglichen: sowohl wenn wir sie als Vorschautafeln verwenden, als auch wenn wir sie als Tafeln verwenden, die simuliert und in Tickets für die Präsentation umgewandelt werden. Daher musste auch die bisherige Funktion zum Laden von Vorschaubalken geändert werden, sodass sie nun wie unten dargestellt aussieht:

bool LoadPrevBars(const string szFileNameCSV)
        {
                int     file,
                        iAdjust = 0;
                datetime dt = 0;
                MqlRates Rate[1];
                                
                if (OpenFileBars(file, szFileNameCSV))
                {
                        Print("Loading preview bars for Replay. Please wait....");
                        while ((!FileIsEnding(file)) && (!_StopFlag))
                        {
                                FileReadBars(file, Rate);
                                iAdjust = ((dt != 0) && (iAdjust == 0) ? (int)(Rate[0].time - dt) : iAdjust);
                                dt = (dt == 0 ? Rate[0].time : dt);
                                CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                        }
                        m_dtPrevLoading = Rate[0].time + iAdjust;
                        FileClose(file);
                        
                        return (!_StopFlag);
                }
                m_dtPrevLoading = 0;
                        
                return false;
        }


Die Funktionsweise dieser Download-Funktion hat sich nicht geändert, obwohl es jetzt mehr Aufrufe gibt. Das Extrahieren der Teile aus der vorherigen Funktion, die im neuen Punkt verwendet werden sollen, gibt uns mehr Sicherheit, da der gesamte Code bereits zuvor getestet wurde. Auf diese Weise müssen wir uns nur noch um neue Funktionen kümmern. Nun, da das Fundament gegossen ist, müssen wir einen neuen Zusatz in die Konfigurationsdatei einfügen. Die Funktion zielt darauf ab, zu bestimmen, welche Balkendateien in Form von Tickets simuliert werden sollen. Zu diesem Zweck müssen wir eine neue Definition hinzufügen:

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


Damit können wir bereits einen einfachen Test durchführen, der genau das ist, was wir brauchen, um mit der Simulation zu beginnen.

                bool SetSymbolReplay(const string szFileConfig)
                        {
#define macroERROR(MSG) { FileClose(file); MessageBox((MSG != "" ? MSG : StringFormat("An 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 the\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
                                                        if (szInfo == def_STR_BarsToTicks) iStage = 4; else
                                                                macroERROR(StringFormat("%s is not recognized in the system\nin line %d.", szInfo, iLine));
                                                        break;
                                                case Transcription_INFO:
                                                        if (szInfo != "") switch (iStage)
                                                        {
                                                                case 0:
                                                                        macroERROR(StringFormat("Command not recognized 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;
                                                                case 4:
                                                                        if (!LoadBarsToTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                        }
                                                        break;
                                        };
                                        iLine++;
                                }
                                FileClose(file);

                                return (!_StopFlag);
#undef macroERROR
                        }


Sehen Sie, wie einfach es ist, dem Code neue Funktionen hinzuzufügen. Allein die Tatsache, dass wir diesen Test hinzufügen, gibt uns die Möglichkeit, noch mehr Aspekte zu analysieren. Von hier aus können wir die gleiche Art von Verhalten betrachten, die wir in anderen Situationen beobachtet haben. Jede in diesem Stadium definierte Datei wird als Balkendatei behandelt, die in Tickets umgewandelt werden muss, was mit diesem Aufruf geschieht.

Auch hier gilt: Wann immer es möglich ist, sollten wir es vermeiden, unnötigen Code zu schreiben. Es ist ratsam, bereits getestete Codes wiederzuverwenden. Genau so haben wir es bis jetzt gemacht. Wir werden jedoch in Kürze ein neues Thema beginnen, das jedoch in einem anderen Artikel behandelt werden soll. Doch zuvor ist es wichtig, einen wesentlichen Aspekt zu verstehen.


Ein paar Gedanken vor der Umsetzung

Vor der Einführung des Umrechnungssystems ist ein Punkt zu beachten. Wissen Sie, wie viele verschiedene Arten von Balken-Konfigurationen es wirklich gibt? Obwohl viele Menschen glauben, dass es viele Arten gibt, können wir alle möglichen Konfigurationen auf nur vier reduzieren. Sie sind in der nachstehenden Abbildung dargestellt:


Warum ist das für uns relevant? Dies ist wichtig, weil es bestimmt, wie viele Optionen wir umsetzen müssen. Wenn wir nicht verstehen, dass es nur diese vier Optionen gibt, laufen wir Gefahr, einige Optionen zu übersehen oder umgekehrt mehr Falltypen als nötig zu schaffen. Ich möchte noch einmal betonen, dass es keine Möglichkeit gibt, ein perfektes simuliertes Modell zu erstellen, um Balken nachzubilden. Man kann höchstens eine mehr oder weniger genaue Schätzung der tatsächlichen Bewegung vornehmen, die zur Bildung dieses bestimmten Balkens geführt hat.

Es gibt einige Details zum zweiten Typ, bei dem der Körper des Balkens nur oben angebracht werden kann, wie auf dem Bild zu sehen. Diese Tatsache hat jedoch keinen Einfluss auf das System, das wir einführen werden. Ebenso spielt es keine Rolle, ob der Balken einen Kauf- oder Verkaufsvorgang darstellt; die Umsetzung bleibt dieselbe. Die einzige Nuance ist, in welche Richtung wir zunächst gehen sollten. Auf diese Weise minimieren wir die Anzahl der Fälle, die wir implementieren müssen. Abgesehen von den in der Abbildung dargestellten Fällen müssen wir aber noch eine weitere Sache verstehen: Wie viele Tickets müssen wir mindestens wirklich erstellen? Das mag für einige verwirrend sein, aber für jemanden, der ein Spiel-/Simulationssystem oder sogar einen Strategietester implementiert, wird das alles einen Sinn ergeben.

Denken wir mal darüber nach: Es ist nicht sinnvoll, nur 1 Ticket in einem System zu verwenden, da dies nur einen Kauf- oder Verkaufshandel und nicht die Bewegung selbst darstellt. Daher können wir diese Möglichkeit ausschließen. Wir könnten uns mindestens zwei Tickets ausdenken, die den Anfangs- und den Schlusspunkt symbolisieren. Obwohl dies logisch erscheint, wird es auch keine wirkliche Bewegung geben, da wir nur einen Tick zum Öffnen des Balkens und einen zweiten zum Schließen des Balkens erzeugen müssen. 

HINWEIS: Wir versuchen nicht, einfach nur Tickets zu generieren, sondern wir wollen eine Bewegung erzeugen, die einen Balken simuliert. Wir werden diese Themen in künftigen Artikeln weiter vertiefen, aber zunächst müssen wir ein Basissystem entwickeln.

Die Mindestanzahl der Tickets beträgt also 3. Daher kann der 1-Minuten-Balken einige der in der vorherigen Abbildung beobachteten Konfigurationen widerspiegeln. Beachten Sie jedoch, dass das Vorhandensein von mindestens 3 Tickets nicht bedeutet, dass sich der Kurs genau 1 Tick nach oben und 1 Tick nach unten oder 3 Ticks nach oben oder unten bewegt hat. Die Bewegung kann von diesem 1 Tick abweichen, da zum Zeitpunkt der Erstellung des Balkens keine Liquidität vorhanden ist.

Das ist wichtig: Einige der hier verwendeten Begriffe können den Leser verwirren. Um Missverständnisse zu vermeiden, sollten wir dies klarstellen: Wenn ich den Begriff TICKET verwende, meine ich eigentlich ein Handelsereignis, d. h. den Kauf oder Verkauf eines Vermögenswerts zu einem bestimmten Preis. Mit dem Begriff TICK meine ich die kleinste Abweichung im Verhältnis zum Handelskurs. Um diesen Unterschied zu verstehen, müssen Sie Folgendes bedenken: 1 Tick am Aktienmarkt kostet 0,01 Punkte, 1 Tick in Dollar-Futures kostet 0,5 Punkte und 1 Tick in Index-Futures kostet 5 Punkte.

Obwohl dies die Dinge in mancher Hinsicht schwieriger machen kann, da die Bewegungssimulation nicht mehr die exakte Realität widerspiegelt, sondern eher eine idealisierte Bewegung, ist es wichtig, diese Tatsache zu erwähnen, um im Hinterkopf zu behalten, dass in vielen Fällen ein Simulationssystem, das 1-Minuten-Balken verwendet, nicht genau reproduziert, was tatsächlich auf dem Markt passiert oder passiert. Daher ist es immer besser, die kürzesten Zeiträume zu verwenden. Da dieser Zeitrahmen 1 Minute beträgt, sollten Sie ihn immer verwenden.

Vielleicht verstehen Sie noch nicht das eigentliche Problem bei der Verwendung von Balken als Methode zur Erstellung von Tickets. Aber bedenken Sie Folgendes: Wenn auf einem realen Markt ein 1-Minuten-Balken zu einem bestimmten Preis eröffnet wird. Aufgrund mangelnder Liquidität springt der Kurs um 3 Ticks und fällt nach einiger Zeit um 1 Tick, wenn er dann an dieser letzten Position schließt, stellt sich die endgültige Situation wie folgt dar:

Das obige Bild kann verwirrend sein und Sie haben es vielleicht nicht richtig verstanden. Sie enthält die folgenden Informationen: In der linken Ecke sehen wir uns die aktuelle Bewegung in Ticks an. Kleine horizontale Balken stellen die einzelnen Ticks dar. Die Kreise stellen die Preise dar, zu denen der Vermögenswert zwischen den Tickets tatsächlich gestoppt wurde. Die grüne Linie zeigt einen Preissprung an. Bitte beachten Sie, dass es Fälle gab, in denen zu bestimmten Ticks kein Handel stattfand, aber bei der Analyse des OHCL-Wertes keine offensichtlichen Tick-Spikes zu erkennen sind. Simuliert man die Bewegung nur mit den Balkenkerzenhaltern, sieht man das folgende Bild:

Die blaue Linie stellt die simulierte Bewegung dar. In diesem Fall werden wir alle Ticks durchgehen, unabhängig davon, was während des Live-Handels tatsächlich passiert ist. Denken Sie also immer daran, dass eine Modellierung nicht dasselbe ist wie die Verwendung echter Daten. Wie komplex ein Modellierungssystem auch sein mag, es wird die Realität nie genau widerspiegeln.


Die Umsetzung des grundlegenden Umrechnungssystems

Wie bereits erwähnt, muss zunächst die Größe des Preisticks bestimmt werden. Zu diesem Zweck müssen wir einige zusätzliche Elemente in die Konfigurationsdatei aufnehmen. Sie müssen von der Klasse C_Replay erkannt werden. Daher müssen wir einige Definitionen und zusätzlichen Code zu dieser Klasse hinzufügen. Wir beginnen mit den folgenden Zeilen.

#define def_STR_FilesBar        "[BARS]"
#define def_STR_FilesTicks      "[TICKS]"
#define def_STR_TicksToBars     "[TICKS->BARS]"
#define def_STR_BarsToTicks     "[BARS->TICKS]"
#define def_STR_ConfigSymbol    "[CONFIG]"
#define def_STR_PointsPerTicks  "POINTSPERTICK"
#define def_Header_Bar          "<DATE><TIME><OPEN><HIGH><LOW><CLOSE><TICKVOL><VOL><SPREAD>"
#define def_Header_Ticks        "<DATE><TIME><BID><ASK><LAST><VOLUME><FLAGS>"
#define def_BarsDiary           540


In dieser Zeile wird eine Zeichenkette definiert, die die zu verarbeitenden Konfigurationsdaten erkennen soll. Es wird auch die erste der Konfigurationen sein, die wir von nun an definieren können. Auch hier müssen wir dem System ein paar Zeilen Code hinzufügen, damit diese Einstellungen interpretiert und angewendet werden können, aber die Ergänzungen sind relativ einfach. Als Nächstes wollen wir uns ansehen, was zu tun ist:

                bool SetSymbolReplay(const string szFileConfig)
                        {
#define macroERROR(MSG) { FileClose(file); MessageBox((MSG != "" ? MSG : StringFormat("An 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 the\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
                                                        if (szInfo == def_STR_BarsToTicks) iStage = 4; else
                                                        if (szInfo == def_STR_ConfigSymbol) iStage = 5; else
                                                                macroERROR(StringFormat("%s is not recognized in the system\nin line %d.", szInfo, iLine));
                                                        break;
                                                case Transcription_INFO:
                                                        if (szInfo != "") switch (iStage)
                                                        {
                                                                case 0:
                                                                        macroERROR(StringFormat("Command not recognized 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;
                                                                case 4:
                                                                        if (!LoadBarsToTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                                case 5:
                                                                        if (!Configs(szInfo)) macroERROR("");
                                                                        break;
                                                        }
                                                        break;
                                        };
                                        iLine++;
                                }
                                FileClose(file);
                                
                                return (!_StopFlag);
#undef macroERROR
                        }


Hier geben wir an, dass alle Informationen aus der nächsten Zeile vom System in der fünften Analysestufe verarbeitet werden. Wenn der Informationseintrag in Schritt 5 ausgelöst und erfasst wird, wird das Verfahren aufgerufen. Um die Beschreibung dieses Verfahrens zu vereinfachen, wird im Folgenden das Verfahren beschrieben, das in der 5. Stufe aufgerufen wird:

inline bool Configs(const string szInfo)
                        {
                                string szRet[];
                                
                                if (StringSplit(szInfo, '=', szRet) == 2)
                                {
                                        StringTrimRight(szRet[0]);
                                        StringTrimLeft(szRet[1]);
                                        if (szRet[0] == def_STR_PointsPerTicks) m_PointsPerTick = StringToDouble(szRet[1]); else
                                        {
                                                Print("Variable >>", szRet[0], "<< not defined.");
                                                return false;
                                        }
                                        return true;
                                }
                                Print("Definition of configuration >>", szInfo, "<< is invalid.");
                                return false;
                        }


Zunächst wird der Name der internen Wiederholungsvariablen erfasst und von dem zu verwendenden Wert getrennt. Dies wurde vom Nutzer in der Replay/Simulations-Konfigurationsdatei festgelegt. Das Ergebnis dieser Operation liefert uns zwei Informationen: erstens den Namen der definierten Variablen und zweitens ihren Wert. Dieser Wert kann je nach Art der Variablen variieren, ist aber für den Nutzer völlig transparent. Sie müssen sich nicht darum kümmern, ob der Typ string, double oder integer ist. Der Typ wird hier im Code ausgewählt.

Bevor Sie diese Daten verwenden, müssen Sie alles entfernen, was für die Basisinformationen nicht relevant ist. In der Regel handelt es sich bei diesem Element um eine Art internes Format, das dem Nutzer das Lesen oder Schreiben von Konfigurationsdateien erleichtern kann. Alles, was nicht verstanden und angewendet wird, gilt als Fehler. Dies führt dazu, dass wir false zurückgeben, was wiederum dazu führt, dass der Dienst beendet wird. Später wird der Grund natürlich in der MetaTrader 5-Plattform gemeldet.

Unsere Konfigurationsdatei wird also wie folgt aussehen. Denken Sie daran, dass dies nur eine Beispielkonfigurationsdatei ist:

[Config]
PointsPerTick = 5

[Bars]
WIN$N_M1_202112060900_202112061824
WIN$N_M1_202112070900_202112071824

[ Ticks -> Bars]

[Ticks]

[ Bars -> Ticks ]
WIN$N_M1_202112080900_202112081824

#End of the configuration file...


Bevor Sie fortfahren, müssen Sie einige letzte Einstellungen an Ihrem System vornehmen. Das ist entscheidend. Wir müssen das System debuggen, um sicherzustellen, dass der Balkenmechanismus in ein simuliertes Ticketmodell umgesetzt wird. Die erforderliche Änderung ist im folgenden Code hervorgehoben:

inline int Event_OnTime(void)
        {
                bool    bNew;
                int     mili, iPos;
                u_Interprocess Info;
                static MqlRates Rate[1];
                static datetime _dt = 0;
                datetime tmpDT = macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                                
                if (m_ReplayCount >= m_Ticks.nTicks) return -1;
                if (bNew = (_dt != tmpDT))
                {
                        _dt = tmpDT;
                        Rate[0].real_volume = 0;
                        Rate[0].tick_volume = 0;
                }
                mili = (int) m_Ticks.Info[m_ReplayCount].time_msc;
                do
                {
                        while (mili == m_Ticks.Info[m_ReplayCount].time_msc)
                        {
                                Rate[0].close = m_Ticks.Info[m_ReplayCount].last;
                                Rate[0].open = (bNew ? Rate[0].close : Rate[0].open);
                                Rate[0].high = (bNew || (Rate[0].close > Rate[0].high) ? Rate[0].close : Rate[0].high);
                                Rate[0].low = (bNew || (Rate[0].close < Rate[0].low) ? Rate[0].close : Rate[0].low);
                                Rate[0].real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real;
                                bNew = false;
                                m_ReplayCount++;
                        }
                        mili++;
                }while (mili == m_Ticks.Info[m_ReplayCount].time_msc);
                Rate[0].time = _dt;
                CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                iPos = (int)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
                GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                if (Info.s_Infos.iPosShift != iPos)
                {
                        Info.s_Infos.iPosShift = (ushort) iPos;
                        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                }
                return (int)(m_Ticks.Info[m_ReplayCount].time_msc < mili ? m_Ticks.Info[m_ReplayCount].time_msc + (1000 - mili) : m_Ticks.Info[m_ReplayCount].time_msc - mili);
        }


Hier gibt es einen interessanten Punkt: Sie können die Visualisierung ausschalten, um zu sehen, wie die Bewegung im Chart verläuft. Es geht darum, die Funktion zu deaktivieren, die die zweiten Werte ausschließt, die in den simulierten Tickets vorhanden sind. In diesem Fall wird jedes simulierte Ticket im Chart als Balken dargestellt.

Dies erscheint zwar etwas ungewöhnlich, aber es hilft uns zu verstehen, was vor sich geht, ohne in eine Protokolldatei schreiben zu müssen. Bei diesem Eintrag müssen wir den Wert für Sekunden entfernen. Wenn wir das nicht tun, dann wird jedes simulierte Ticket einen Balken erzeugen, und das brauchen wir letztlich nicht. Wir wollen, dass die 1-Minuten-Balken so aussehen, als ob sie echt wären.


Schließlich kommen wir zur Umsetzungsphase

Bevor wir uns dem Code zuwenden, wollen wir uns die Bewegung ansehen, die ich in diesem Artikel vorstellen werde. In Zukunft werde ich dem Leser zeigen, wie man es komplexer gestalten kann, aber zunächst ist es wichtig, dass es richtig funktioniert. Nachfolgend sehen Sie, wie diese Bewegung ablaufen wird:

Dies mag zwar einfach erscheinen, aber wir werden den Balken in eine simulierte Ticketbewegung umwandeln. Da die Bewegung nie linear ist, sondern in einer Art Zickzack verläuft, habe ich beschlossen, einen Satz mit drei solchen Schwingungen zu komponieren. Sie können diese Zahl erhöhen, wenn Sie dies wünschen. In zukünftigen Artikeln werde ich Ihnen zeigen, wie Sie diese grundlegende Technik in etwas viel Komplexeres verwandeln können.

Da wir nun wissen, wie diese Bewegung aussehen wird, können wir mit dem Code fortfahren. Die erste Funktion, die für die Erstellung des Konvertierungssystems erforderlich ist, wird im folgenden Code gezeigt:

                bool LoadBarsToTicksReplay(const string szFileNameCSV)
                        {
//#define DEBUG_SERVICE_CONVERT
                                int file, max;
                                MqlRates rate[1];
                                MqlTick tick[];
                                
                                if (OpenFileBars(file, szFileNameCSV))
                                {
                                        Print("Converting bars to ticks. Please wait...");
                                        ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                                        ArrayResize(m_Ticks.Rate, def_BarsDiary);
                                        ArrayResize(tick, def_MaxSizeArray);
                                        while ((!FileIsEnding(file)) && (!_StopFlag))
                                        {
                                                FileReadBars(file, rate);
                                                max = SimuleBarToTicks(rate[0], tick);
                                                for (int c0 = 0; c0 <= max; c0++)
                                                {
                                                        ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                                                        m_Ticks.Info[m_Ticks.nTicks++] = tick[c0];
                                                }
                                        }
                                        FileClose(file);
                                        ArrayFree(tick);
                                        
#ifdef DEBUG_SERVICE_CONVERT
        file = FileOpen("Infos.txt", FILE_ANSI | FILE_WRITE);
        for (long c0 = 0; c0 < m_Ticks.nTicks; c0++)
                FileWriteString(file, StringFormat("%s.%03d %f --> %f\n", TimeToString(m_Ticks.Info[c0].time, TIME_DATE | TIME_SECONDS), m_Ticks.Info[c0].time_msc, m_Ticks.Info[c0].last, m_Ticks.Info[c0].volume_real));
        FileClose(file);
#endif

                                        return (!_StopFlag);
                                }
                                
                                return false;
                        }


Obwohl sie einfach erscheint, ist diese Funktion der erste Schritt, der für die Erstellung einer Simulation verantwortlich ist. Wenn wir sie sehen, können wir zumindest ein wenig verstehen, wie das System die Simulation durchführt. Es ist wichtig zu beachten, dass im Gegensatz zum Strategietester hier die Simulation jedes Minutenbalkens bei der Nutzung des Dienstes etwa eine Minute dauert, bis er vollständig auf dem Bildschirm angezeigt wird. Dies geschieht absichtlich. Wenn es also darum geht, eine Strategie zu testen, empfehle ich Ihnen, das auf der MetaTrader 5-Plattform verfügbare Tool zu verwenden und dieses Tool (dessen Entwicklung wir zeigen) nur für das manuelle Training und Testen zu nutzen.

Es gibt einen Aspekt, den es in Zukunft nicht mehr geben wird, der aber in diesem Moment entscheidend ist. Dies bezieht sich auf die Definition, die wir gerade erörtern, die es Ihnen ermöglicht, eine Datei mit simulierten Ticketdaten zu erstellen, um den Grad der Komplexität des simulierten Systems zu analysieren. Es ist wichtig zu beachten, dass die erstellte Datei nur die für die Analyse notwendigen Daten enthält, ohne zusätzliche oder unnötige Informationen.

Der Rest der Funktion ist ziemlich intuitiv, da der Code bereits vorher existierte. Jetzt haben wir einen Anruf, den wir später noch genauer betrachten werden. Nach diesem Aufruf, der die Tickets simuliert, wird eine kleine Schleife ausgeführt, um die Tickets in der Datenbank zu speichern. Sie ist leicht zu verstehen und bedarf keiner weiteren Erklärung.

inline int SimuleBarToTicks(const MqlRates &rate, MqlTick &tick[])
                        {
                                int t0 = 0;
                                long v0, v1, v2, msc;
                                                                
                                m_Ticks.Rate[++m_Ticks.nRate] = rate;
                                Pivot(rate.open, rate.low, t0, tick);
                                Pivot(rate.low, rate.high, t0, tick);
                                Pivot(rate.high, rate.close, t0, tick, true);
                                v0 = (long)(rate.real_volume / (t0 + 1));
                                v1 = 0;
                                msc = 5;
                                v2 = ((60000 - msc) / (t0 + 1));
                                for (int c0 = 0; c0 <= t0; c0++, v1 += v0)
                                {
                                        tick[c0].volume_real = (v0 * 1.0);
                                        tick[c0].time = rate.time + (datetime)(msc / 1000);
                                        tick[c0].time_msc = msc % 1000;
                                        msc += v2;
                                }
                                tick[t0].volume_real = ((rate.real_volume - v1) * 1.0);
                                
                                return t0;
                        }


Diese Eigenschaft macht die Aufgabe etwas schwieriger und damit interessanter. Gleichzeitig hilft es uns, alle notwendigen Strukturen in einer organisierten und überschaubaren Weise zusammenzustellen. Um die Komplexität dieser Funktion zu verringern, habe ich eine weitere Prozedur erstellt, die dreimal aufgerufen wird. Wenn Sie die Dokumentation Reale und generierte Ticks - Algorithmischer Handel lesen, werden Sie feststellen, dass das System darin nicht drei, sondern vier Mal aufgerufen wird. Sie können weitere Aufrufe hinzufügen, wenn Sie dies wünschen, aber wie gesagt, werde ich einen Weg aufzeigen, wie Sie die Komplexität dieses Systems erhöhen können, ohne zusätzliche Aufrufe zur Pivot-Prozedur hinzufügen zu müssen.

In Bezug auf das oben erwähnte Verfahren werden wir nach drei Aufrufen von Pivot eine Anzahl von simulierten Tickets haben, die davon abhängen, wie die Teilung durchgeführt wurde. Damit können wir nun kleine Korrekturen an den 1-Minuten-Balken vornehmen, die es uns ermöglichen, die Originaldaten in irgendeiner Weise zu verwenden. Der erste Schritt besteht darin, eine einfache Division des realen Volumens vorzunehmen, sodass jeder simulierte Tick einen Bruchteil des Gesamtvolumens enthält. Anschließend nehmen wir eine kleine Anpassung des Timings jedes simulierten Ticks vor. Nachdem wir festgelegt haben, welche Brüche verwendet werden sollen, können wir eine Schleife einrichten und sicherstellen, dass jeder Bruch in dem entsprechenden Ticket gespeichert wird. Im Moment bleiben wir dabei, dass das System tatsächlich funktionieren muss. Die oben genannten Funktionen können jedoch noch verbessert werden, was die Sache noch interessanter macht. Anders als bei der Uhrzeit muss das Volumen korrigiert werden, um mit dem Original identisch zu bleiben. Aufgrund dieses Details haben wir die letzte Berechnung in diesem Verfahren, und dies macht die Korrektur, sodass das endgültige Volumen der 1-Minuten-Balken, ist das gleiche wie das anfängliche Volumen.

Schauen wir uns nun die letzte Funktion in diesem Artikel an, mit der ein Pivot-Punkt auf der Grundlage der im obigen Code angegebenen Werte und Parameter erstellt wird. Es ist wichtig zu wissen, dass die Werte an die eigenen Interessen angepasst werden können, aber es muss darauf geachtet werden, dass die anschließende Funktion korrekt funktioniert.

//+------------------------------------------------------------------+
#define macroCreateLeg(A, B, C) if (A < B)      {               \
                while (A < B)   {                               \
                        tick[C++].last = A;                     \
                        A += m_PointsPerTick;                   \
                                }                               \
                                                } else {        \
                while (A > B)   {                               \
                        tick[C++].last = A;                     \
                        A -= m_PointsPerTick;                   \
                                }               }
                        
inline void Pivot(const double p1, const double p2, int &t0, MqlTick &tick[], bool b0 = false)
                        {
                                double v0, v1, v2;
                                
                                v0 = (p1 > p2 ? p1 - p2 : p2 - p1);
                                v1 = p1 + (MathFloor((v0 * 0.382) / m_PointsPerTick) * m_PointsPerTick * (p1 > p2 ? -1 : 1));
                                v2 = p1 + (MathFloor((v0 * 0.618) / m_PointsPerTick) * m_PointsPerTick * (p1 > p2 ? -1 : 1));
                                v0 = p1;
                                macroCreateLeg(v0, v2, t0);
                                macroCreateLeg(v0, v1, t0);
                                macroCreateLeg(v0, p2, t0);
                                if (b0) tick[t0].last = v0;
                        }
#undef macroCreateLeg
//+------------------------------------------------------------------+


Die oben genannte Funktion ist einfach in der Bedienung. Auch wenn die Berechnungen seltsam anmuten mögen, werden Sie bei der Betrachtung der Werte, die zur Erstellung des Pivot-Punktes verwendet werden, feststellen, dass wir immer versuchen, den Pivot-Punkt anhand der ersten und dritten Fibonacci-Linie festzulegen. Zunächst ist es wichtig zu wissen, dass es keine Rolle spielt, ob der Drehpunkt nach oben oder unten zeigt; die Funktion führt die Berechnungen entsprechend durch. Dann kommt ein Aspekt, der für Menschen mit geringen Programmierkenntnissen verwirrend sein könnte: MACRO. Der Grund für die Verwendung des Makros ist, dass es einfacher ist, einen Teil eines Pivots mit Hilfe eines Makros zu erstellen. Sie können aber auch eine Funktion für diese Aufgabe erstellen. Wenn wir reines C++ verwenden würden, hätte dieses Makro wahrscheinlich einen völlig anderen Code. Aber hier, bei der Verwendung von MQL5, wie es erstellt wurde, funktioniert es als Workaround.

Die Verwendung dieses Makros ist wesentlich effizienter als die Einbettung von Code in den Bereichen, in denen er deklariert ist.


Schlussfolgerung

Wir sollten einen leicht zu lesenden und verständlichen Code immer einem Code vorziehen, der Korrekturen oder Änderungen erfordert und uns zwingt, stundenlang über unsere Aktionen nachzudenken. Damit ist dieser Artikel abgeschlossen. Das nachstehende Video zeigt das Ergebnis im aktuellen Entwicklungsstadium, das mit dem dem Artikel beigefügten Material erstellt wurde.

Ich möchte jedoch auf ein Problem hinweisen, das bei der Verwendung simulierter Tickets aufgetreten ist. Dies bezieht sich auf ein System, mit dem eine andere Position als die, an der sich der Replay-Dienst befindet, verschoben oder gesucht werden kann. Dieses Problem tritt nur auf, wenn Sie simulierte Tickets verwenden. Im nächsten Artikel werden wir dieses Problem angehen und beheben sowie weitere Verbesserungen vornehmen.



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

Beigefügte Dateien |
Market_Replay.zip (50.42 KB)
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.
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
Hier werden wir uns ansehen, wie wir zuverlässigere Daten (gehandelte Ticks) im Wiedergabesystem verwenden können, ohne uns Gedanken darüber zu machen, ob sie angepasst sind oder nicht.
Strukturen in MQL5 und Methoden zum Drucken deren Daten Strukturen in MQL5 und Methoden zum Drucken deren Daten
In diesem Artikel werden wir uns die Strukturen von MqlDateTime, MqlTick, MqlRates und MqlBookInfo ansehen sowie die Methoden zum Drucken von deren Daten. Um alle Felder einer Struktur auszudrucken, gibt es die Standardfunktion ArrayPrint(), die die im Array enthaltenen Daten mit dem Typ der behandelten Struktur in einem praktischen Tabellenformat anzeigt.
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.