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

Entwicklung eines Replay-Systems — Marktsimulation (Teil 12): Die Geburt des SIMULATORS (II)

MetaTrader 5Tester | 21 November 2023, 08:50
247 0
Daniel Jose
Daniel Jose

Einführung

Im vorigen Artikel Entwicklung eines Replay-Systems — Marktsimulation (Teil 11): Die Geburt des SIMULATORS (I) haben wir unser Replay-/Simulationssystem in die Lage versetzt, 1-Minuten-Balken zu verwenden, um mögliche Marktbewegungen zu simulieren. Aber vielleicht haben Sie nach der Lektüre dieses Materials bemerkt, dass die Bewegungen nicht so sehr mit den Bewegungen des realen Marktes übereinstimmen. In diesem Artikel habe ich die Punkte aufgezeigt, die geändert werden müssen, damit das System noch näher an der Realität des Marktes ist. Doch egal, wie viele Versuche und Experimente Sie mit einfachen Methoden unternehmen, Sie werden nicht in der Lage sein, etwas Ähnliches wie mögliche und wahrscheinliche Marktbewegungen zu erzeugen.


Beginn der Umsetzung

Um alles Notwendige zu tun und das System etwas komplexer zu machen, werden wir die Zufallszahlengenerierung verwenden. Dadurch werden die Dinge weniger vorhersehbar und das Replay-/Simulationssystem wird interessanter. Nach den Hinweisen zur Erzeugung von Zufallszahlen in der MQL5-Dokumentation müssen wir mehrere Schritte durchführen, die auf den ersten Blick recht einfach sind. Es gibt keinen Grund zur Sorge, es ist eigentlich ganz einfach. Zunächst werden wir den Code folgendermaßen ergänzen:

void InitSymbolReplay(void)
        {
                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);
        }

Hier halten wir uns strikt an die Tipps aus der Dokumentation. Sie können dies überprüfen, indem Sie sich die Funktion srand ansehen, die die Erzeugung von Pseudozufallszahlen initialisiert. Wie in der Dokumentation selbst erläutert, wird ein fester Wert im Aufruf verwendet, zum Beispiel:

srand(5);

Wir erhalten immer die gleiche Nummernfolge. Auf diese Weise stoppen wir die Zufallsgenerierung und erhalten eine „vorhersehbare“ Sequenz. Beachten Sie, dass ich das Wort „vorhersehbar“ in Anführungszeichen gesetzt habe, weil die Reihenfolge immer die gleiche sein wird. Solange die gesamte Generierungsschleife nicht abgeschlossen ist, wissen wir jedoch nicht genau, wie der nächste Wert lauten wird. In gewisser Weise kann dies interessant sein, wenn wir eine Simulation erstellen wollen, bei der die simulierte Abfolge immer gleich ist. Andererseits macht es dieser Ansatz sehr einfach, sodass es unmöglich ist, mit dem System eine gute Lernerfahrung zu machen.

Wenn Sie den Tester zur Erstellung von nutzerdefinierten Studien verwenden, ist es nicht sinnvoll, eine große Anzahl verschiedener Dateien zu erstellen. Wir können nur eine Datei erstellen und diese zur Einführung des Zufalls verwenden. Aus diesem Grund werde ich beim Aufruf von srand keinen festen Wert angeben. Das soll der Zufall regeln. Dies bleibt jedoch jedem selbst überlassen.


Lassen Sie uns mit einer komplexeren Art der Aufgabenerfüllung experimentieren.

Als Erstes beseitigen wir die Tatsache, dass wir mit der Suche nach einem Minimum beginnen. Wenn Sie dies wissen, wird alles sehr einfach sein. Wir warten einfach, bis der neue Balken geöffnet wird, und führen eine Verkaufsoperation durch. Wenn der Eröffnungspreis überschritten wird, werden wir sie durchführen. Aber das ist kein Training, das ist eigentlich Betrug.

Hinweis: Einige Expert Advisors können solche Dinge analysieren und bemerken, dies geschieht im Strategie-Tester. Die Tatsache, dass der Expert Advisor dies bemerken kann, macht jede durchgeführte Simulation ungültig.

Um dies zu erreichen, müssen wir die Situation verkomplizieren. Wir werden eine sehr einfache, aber sehr effektive Methode anwenden. Schauen wir uns den folgenden Code an.

inline int SimuleBarToTicks(const MqlRates &rate, MqlTick &tick[])
                        {
                                int t0 = 0;
                                long v0, v1, v2, msc;
                                bool b1 = ((rand() & 1) == 1);
                                double p0, p1;
                                                                
                                m_Ticks.Rate[++m_Ticks.nRate] = rate;
                                p0 = (b1 ? rate.low : rate.high);
                                p1 = (b1 ? rate.high : rate.low);
                                Pivot(rate.open, p0, t0, tick);
                                Pivot(p0, p1, t0, tick);
                                Pivot(p1, 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;
                        }

Bitte haben Sie keine Angst vor dem, was die obige Funktion bewirkt, denn es bleibt alles beim Alten. Die einzige Änderung besteht darin, dass wir jetzt nicht wissen, ob der Balken nach einem Minimum oder einem Maximum suchen wird. In einem ersten Schritt wird geprüft, ob der zufällig erzeugte Wert gerade oder ungerade ist. Sobald wir dies wissen, tauschen wir einfach die Werte aus, wodurch unser Umkehrpunkt entsteht. Beachten Sie jedoch, dass der Umkehrpunkt immer noch auf dieselbe Weise erstellt wird. Das Einzige, was wir nicht wissen werden, ist, ob der Balken nach oben geht, weil er bereits das Minimum erreicht hat, oder ob er nach unten geht, weil er bereits das Maximum erreicht hat.

Dies ist der Anfang. Wir müssen eine weitere Änderung vornehmen, bevor wir zum nächsten Schritt übergehen. Worin besteht die Veränderung? In der vorherigen Version gab es normalerweise 9 Segmente zwischen dem Öffnen und dem Schließen des Balkens, aber mit ein wenig Code werden wir diese 9 Segmente in 11 Segmente verwandeln. Aber wie? Sehen Sie sich den nachstehenden Code an:

#define def_NPASS 3
inline int SimuleBarToTicks(const MqlRates &rate, MqlTick &tick[])
                        {
                                int t0 = 0;
                                long v0, v1, v2, msc;
                                bool b1 = ((rand() & 1) == 1);
                                double p0, p1, p2;
                                                                
                                m_Ticks.Rate[++m_Ticks.nRate] = rate;
                                p0 = (b1 ? rate.low : rate.high);
                                p1 = (b1 ? rate.high : rate.low);
                                p2 = floor((rate.high - rate.low) / def_NPASS);
                                Pivot(rate.open, p0, t0, tick);
                                for (int c0 = 1; c0 < def_NPASS; c0++, p0 = (b1 ? p0 + p2 : p0 - p2)) Pivot(p0, (b1 ? p0 + p2 : p0 - p2), t0, tick);
                                Pivot(p0, p1, t0, tick);
                                Pivot(p1, 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;
                        }
#undef def_NPASS

Man könnte meinen, dass er derselbe sei, aber in Wirklichkeit gibt es einen großen Unterschied. Obwohl wir nur eine Variable hinzugefügt haben, um den Zwischenpunkt darzustellen, können wir, sobald wir diesen Punkt gefunden haben, zwei weitere Segmente hinzufügen. Beachten Sie, dass wir, um diese beiden Segmente hinzuzufügen, weiterhin fast den gleichen Code ausführen werden. Bitte beachten Sie, dass die Komplexität, die wir bei der Bildung eines Balkens bei der Erstellung einer Simulation einführen, schnell zunimmt, und zwar nicht im gleichen Maße, wie wir den Code vergrößern. Ein kleines Detail, auf das wir achten sollten, ist, dass die Definition nicht auf Null gesetzt werden sollte. In diesem Fall kommt es zu einem Fehler bei der Division durch Null. In diesem Fall sollten wir mindestens den Wert 1 in der Definition verwenden. Wenn Sie jedoch einen beliebigen Wert von 1 bis zu einem beliebigen Maximum festlegen, können Sie weitere Segmente hinzufügen. Da die Bewegungen in der Regel nicht groß genug sind, um mehr Segmente zu erstellen, ist ein Wert von 3 ausreichend.

Um zu verstehen, was hier passiert ist, sehen Sie sich die folgenden Bilder an.

Bevor Sie neue Segmente hinzufügen


Es hat zwar alles gut funktioniert, aber wenn wir die Version verwenden, die es uns erlaubt, die Amplitude in Bereiche zu unterteilen, ergibt sich folgendes Szenario:

Abbildung 02

Nach der Änderung beginnen wir, die Spanne der Balken durch 3 zu teilen


Beachten Sie, dass die Komplexität etwas zugenommen hat. Ich konnte jedoch keinen großen Vorteil bei der Aufteilung in mehr als 3 Segmente feststellen. Obwohl die Dinge also bereits ziemlich interessant geworden sind, erzeugt das System nicht so viel Komplexität mehr, wie es könnte. Wir werden also einen anderen Ansatz wählen müssen. Dadurch wird der Code nicht wesentlich komplexer. Die Idee ist, eine exponentielle Zunahme der Komplexität zu erreichen, ohne den Code übermäßig zu verkomplizieren.

Um dies zu erreichen, werden wir einen völlig anderen Ansatz wählen. Aber lassen Sie uns zunächst etwas betrachten, das eine Erklärung verdient. Auf diese Weise können wir wirklich verstehen, warum wir unseren Ansatz zur Lösung des Problems ändern.

Wenn Sie die im vorherigen Schritt vorgenommenen Änderungen beachtet haben, ist Ihnen vielleicht etwas Interessantes im endgültigen Code aufgefallen. Für einen Moment haben wir die Kontrolle über den gesamten Körper der Stange und können mit ihr machen, was wir wollen. Im Gegensatz zu anderen Zeiten haben wir eine relativ gerichtete Bewegung, von der Eröffnung bis zum Hoch oder Tief. Wenn wir am gesamten Körper der Stange arbeiten müssen, tun wir nur sehr wenig im Inneren der Stange. Wie sehr wir uns auch bemühen, wir bleiben immer wieder in derselben Situation stecken, aber wenn Sie genau hinsehen, werden Sie feststellen, dass wir immer zwei Werte haben, an denen wir arbeiten können. Dies sind der Anfangs- und der Endpunkt. Warum sollten Sie diesem Moment Aufmerksamkeit schenken? Denken Sie einen Moment darüber nach: Wir haben 60 Tausend Millisekunden, um einen 1-Minuten-Balken zu erstellen. Wenn wir am Anfang des Balkens einen Spielraum von 5 Millisekunden lassen, haben wir immer noch viel Zeit. Wenn wir einige einfache Berechnungen durchführen, werden wir feststellen, dass wir eine Menge Zeit verschwenden, die wir nutzen könnten, um die Balkensimulation viel komplexer zu gestalten.

Wir können eine mögliche Lösung finden: Wenn wir dem Preis 1 Sekunde Zeit lassen, um den Eröffnungspunkt zu verlassen und sich in Richtung Hoch oder Tief zu bewegen, und dem Preis 1 Sekunde Zeit lassen, um sich von dort zum Schlusspunkt zu bewegen, haben wir 58 Sekunden Zeit, um die gewünschte Komplexität zu erzeugen. Beachten Sie jedoch, was über die letzte Sekunde gesagt wurde: „der Preis bewegt sich von dort, wo er ist und geht zum Schlusspunkt.“ Es ist wichtig zu erkennen und zu verstehen, was genau gesagt wurde. Unabhängig davon, was die meiste Zeit passiert, sollten wir immer eine gewisse Zeitspanne reservieren, bis der Kurs schließlich den Schlusspunkt erreicht.

Sie werden eine Bewegung bemerken, die in einer längeren Zeitspanne auftritt, nämlich bei etwas mehr als 33 Millisekunden oder 30 Hz. Wenn wir die maximale Dauer jedes Ticks auf 30 Millisekunden festlegen, werden Sie feststellen, dass die Bewegung der Bewegung des Vermögenswerts recht ähnlich ist. Ein wichtiges Detail: Diese Wahrnehmung ist sehr relativ, denn manche Leute finden es schwierig, einen Vermögenswert zu handeln, der sich aufgrund seiner hohen Volatilität sehr schnell bewegt.

Aus diesem Grund sollte das Replay-/Simulationssystem nicht wirklich als gutes Lernen angesehen werden. Es sei denn, wir verwenden tatsächlich eine Datei mit tatsächlich gehandelten Ticks. Wenn Sie solche Ticks simulieren, kann der falsche Eindruck entstehen, dass alle Preisbereiche besucht werden. Derzeit erlaubt dieses System nicht, 1-Minuten-Balken so zu simulieren, dass Lücken entstehen, obwohl diese Lücken auf dem realen Markt tatsächlich zu ganz bestimmten Zeiten auftreten. Dies sind sehr gefährliche Zeiten, um einen Handel zu eröffnen oder zu schließen, da die Wahrscheinlichkeit, dass der Auftrag außerhalb des gewünschten Preises ausgeführt wird, sehr hoch ist, und die Wahrscheinlichkeit, dass er einfach verfehlt wird, ebenfalls sehr groß ist, da die Volatilität sehr hoch sein kann, wodurch alles auf unerwartete Weise perfekt funktioniert.

Man könnte meinen, ich würde eine Methode verwenden, die immer die minimale Anzahl von Ticks erzeugt, aber ich werde diesen Ansatz noch nicht verwenden. Sie müssen jedoch bedenken, dass es in keiner Weise möglich ist, die tatsächlichen Marktbewegungen durch Simulationen nachzubilden. Wir können nur abschätzen, wie hoch die möglichen Bewegungen sein könnten. Doch bevor wir fortfahren, müssen wir uns auf die Lösung einiger spezifischer Probleme konzentrieren. Wir beginnen mit einem etwas fortgeschritteneren Thema, das aber als Grundlage für unseren Simulator dienen wird. Doch bevor wir fortfahren, müssen wir uns auf die Lösung einiger spezifischer Probleme konzentrieren.


Wenn es keine Ticks gibt, warum ist der Dienst dann aktiv?

Trotz aller Komplexität, die wir lösen müssen, bevor wir uns der Realität annähern können, müssen wir einzelne Probleme lösen, die schon lange aufgeschoben wurden und die wirklich gelöst werden müssen. Das erste dieser Probleme besteht darin, dass wir, wenn wir das System starten, ohne die Vorschaubalken zu laden, keinen Zugriff auf den Kontrollindikator haben. Dieser Fehler ist schon seit einiger Zeit im System vorhanden, aber da die Vorschaubalken vorher immer vorhanden waren, habe ich es aufgeschoben, ihn zu beheben, wie es das System erfordert. Lassen Sie uns nun dieses Problem lösen. Zu diesem Zweck müssen wir an einer ganz bestimmten Stelle unseres Systems einige kleinere Ergänzungen vornehmen. Wir haben dies getan, um die Aufgabe so weit wie möglich zu vereinfachen, siehe unten, was wir tun werden:

                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;
                                bool    bBarPrev;
                                MqlRates rate[1];
                                
                                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;
                                bBarPrev = false;
                                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("Couldn't recognize command in line %d\nof configuration file.", iLine));
                                                                        break;
                                                                case 1:
                                                                        if (!LoadPrevBars(szInfo)) macroERROR("");
                                                                        bBarPrev = true;
                                                                        break;
                                                                case 2:
                                                                        if (!LoadTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                                case 3:
                                                                        if (!LoadTicksReplay(szInfo, false)) macroERROR("");
                                                                        bBarPrev = true;
                                                                        break;
                                                                case 4:
                                                                        if (!LoadBarsToTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                                case 5:
                                                                        if (!Configs(szInfo)) macroERROR("");
                                                                        break;
                                                        }
                                                        break;
                                        };
                                        iLine++;
                                }
                                FileClose(file);
                                if (m_Ticks.nTicks <= 0)
                                {
                                        MessageBox("No ticks to be used.\nClose the service...", "Market Replay", MB_OK);
                                        return false;
                                }
                                if (!bBarPrev)
                                {
                                        rate[0].close = rate[0].open =  rate[0].high = rate[0].low = m_Ticks.Info[0].last;
                                        rate[0].tick_volume = 0;
                                        rate[0].real_volume = 0;
                                        rate[0].time = m_Ticks.Info[0].time - 60;
                                        CustomRatesUpdate(def_SymbolReplay, rate, 1);
                                }
                                
                                return (!_StopFlag);
#undef macroERROR
                        }

Zunächst werden wir zwei neue Variablen für den lokalen Gebrauch definieren. Anschließend werden sie mit dem Wert false initialisiert, um anzuzeigen, dass keine Vorschaubalken geladen sind. Wenn nun zu irgendeinem Zeitpunkt ein vorheriger Balken geladen wird, wird diese Variable auf true gesetzt. Auf diese Weise weiß das System, dass wir den vorherigen Balken geladen haben, und wir lösen damit einen Teil unseres ersten Problems. Wir müssen aber noch prüfen, ob eine Datei geladen ist, die die verwendeten Ticks erzeugt. Wenn keine Ticks vorhanden ist, hat es keinen Sinn, den Dienst zu starten. Daher wird der Dienst gestoppt. Wenn nun Ticks vorhanden sind, prüfen wir, ob eine Art vorheriger Balken geladen wurde. Ist dies nicht der Fall, wird ein leerer Balken initialisiert. Ohne diese Initialisierung können wir nicht auf den Kontrollindikator zugreifen, selbst wenn der Dienst zur Verfügung steht.

Wenn Sie jedoch die oben genannten Korrekturen vornehmen, wird sich alles klären. 


Umsetzung von TICK VOLUME

Der nächste Punkt auf der Liste, der korrigiert werden muss, ist das System, das das Volumen der gehandelten Ticks anzeigt. Viele Leute möchten einen Volumenindikator auf dem Chart haben, und bisher ist das einzige tatsächlich implementierte Volumen das reale Volumen. Das heißt, das Volumen durch die Anzahl der ausgeführten Kontrakte. Genauso wichtig ist aber auch das Tick-Volumen. Wissen Sie, was der Unterschied zwischen den beiden ist? Sehen Sie sich das folgende Bild an:

Sie können darin zwei Volumenwerte sehen. Das eine ist das Tick-Volumen und das andere das Volumen (in diesem Fall das tatsächliche Volumen). Aber wenn ich mir dieses Bild ansehe, können Sie mir den Unterschied zwischen echtem und Tick-Volumen erklären? Wenn Sie den Unterschied nicht kennen, ist es jetzt an der Zeit, ihn endlich herauszufinden.

VOLUME oder REAL VOLUME ist im Wesentlichen die Anzahl der zu einem bestimmten Zeitpunkt gehandelten Kontrakte. Es handelt sich dabei immer um ein Vielfaches des Wertes, der vom jeweiligen Vermögenswert abhängt. Bei einigen Vermögenswerten ist es beispielsweise nicht möglich, mit Werten unter 5 zu handeln, während andere nur Bruchteile davon akzeptieren. Versuchen Sie nicht zu verstehen, warum dies möglich ist, sondern wissen Sie einfach, dass Sie mit Bruchteilen von Werten handeln können. Dieser Wert ist leicht zu verstehen und wird vielleicht deshalb von vielen Menschen verwendet. Wenn wir nun den Wert des REAL VOLUME nehmen und ihn mit dem Mindestwert jedes Kontrakts multiplizieren, erhalten wir einen anderen Wert namens FINANZVOLUMEN. MetaTrader 5 bietet diesen Wert nicht direkt an, aber wie Sie gesehen haben, ist er leicht zu erhalten. Der Handelsserver versteht also, dass er dieses Finanzvolumen nicht zu melden braucht FINANZVOLUMEN an die Handelsterminals zu melden. Programmierer oder Plattformnutzer müssen die angegebene Berechnung implementieren.

Jetzt wird TICK VOLUME zu einem völlig andere Volumen. Es wird nur aus einem einfachen Grund als Teil der Balken angegeben: Wir können nicht erkennen, was während des Handels passiert ist, wenn wir nur das tatsächliche Volumen betrachten. Wir benötigen zusätzliche Informationen - das Tick-Volumen. Aber warum ist das Tick-Volumen verfügbar, wenn wir die Balken abfragen und nicht, wenn wir die Ticks abfragen? Welches Volumen erscheint, wenn wir Ticks anfordern? Falls Ihnen das noch nie aufgefallen ist (oder Sie es noch nicht gesehen haben), können Sie sich das folgende Bild ansehen:

Auch hier stellen die im Feld VOLUME angegebenen Werte NICHT das Tick-Volumen dar. Dieser Wert ist das REAL VOLUME. Wie können wir jedoch das Tick-Volumen herausfinden, wenn es bei der Abfrage von Ticks nicht angegeben wird? Sie erscheint nur bei der Abfrage nach Balken. Der Punkt ist, dass genauso wie der Server versteht, dass er kein FINANZVOLUMEN bereitstellen muss, er auch versteht, dass wir durch die Bereitstellung von gehandelten Ticks in der Lage sein werden, das TICK-VOLUMEN zu berechnen. Dies unterscheidet sich von dem, was passieren würde, wenn wir Balken anfordern, obwohl wir keinen Zugang zu den tatsächlich gehandelten Ticks haben.

Verstehen Sie immer noch nicht? Mit den Daten über die tatsächlich gehandelten Ticks können wir das Tick-Volumen berechnen. Aber wie? Gibt es eine geheimnisvolle Formel? Denn jedes Mal, wenn ich es versuche, passen die Werte nicht. Beruhigen Sie sich, lieber Leser. Es gibt keine Zauberformel. Der Punkt ist, dass Sie wahrscheinlich nicht ganz verstehen, was das TICK VOLUME wirklich ist. In diesem und früheren Artikeln haben wir die Methode der Modellierung der Bewegung innerhalb eines Minutenbalkens verwendet. Obwohl sich diese Bewegung auf alle Preise auswirkt, ist das tatsächliche Tick-Volumen, das wir erzeugen, viel geringer als das Tick-Volumen, das auf dem 1-Minuten-Balken angezeigt wird.

Aber warum? Machen Sie sich keine Sorgen. Sie werden dies im nächsten Artikel besser verstehen, in dem wir das gleiche Tick-Volumen modellieren werden. Ich denke, Sie wissen jetzt, was ein Tick-Volumen ist. Das Tick-Volumen ist die Anzahl der Abschlüsse, die innerhalb eines bestimmten Balkens tatsächlich getätigt wurden. Unser Durchschnittsvolumen liegt bei etwa 150. Tatsächlich liegt der Durchschnitt oft bei 12.890.

Aber vielleicht denken Sie jetzt: Wie kann ich dann dieses Tick-Volumen berechnen? Das ist sehr einfach zu machen. Wir wollen sehen, ob unser System diese Berechnung durchführen kann. Denn um dies zu verstehen, muss man die Berechnungen in Aktion sehen.

Derzeit wird diese Berechnung aus unterschiedlichen Gründen an zwei Stellen vorgenommen. Die erste Stelle ist unten abgebildet:

inline bool BuiderBar1Min(MqlRates &rate, const MqlTick &tick)
                        {
                                if (rate.time != macroRemoveSec(tick.time))
                                {
                                        rate.real_volume = 0;
                                        rate.tick_volume = 0;
                                        rate.time = macroRemoveSec(tick.time);
                                        rate.open = rate.low = rate.high = rate.close = tick.last;
                
                                        return true;
                                }
                                rate.close = tick.last;
                                rate.high = (rate.close > rate.high ? rate.close : rate.high);
                                rate.low = (rate.close < rate.low ? rate.close : rate.low);
                                rate.real_volume += (long) tick.volume_real;
                                rate.tick_volume += (tick.last > 0 ? 1 : 0);

                                return false;
                        }

In diesem Stadium berechnen wir das Volumen der Ticks, die im Balken vorhanden sein werden. Die zweite Stelle ist folgende:

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;
                                                Rate[0].tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > 0 ? 1 : 0);
                                                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);
                        }

In diesem Stadium tun wir dasselbe, d. h. wir berechnen das Tick-Volumen. Ist das wirklich wahr? Ja, das ist richtig. Das Tick-Volumen wird berechnet, indem tatsächlich nur die Ticks berücksichtigt werden, die ausgeführte Geschäfte anzeigen. Das heißt, ein Tick pro Vorgang. Das bedeutet, dass die Ticks, bei denen die Flags BID oder ASK aktiv sind, nicht an der Berechnung teilnehmen, und nur die Ticks, die eine SELL- oder BUY-Flag haben, werden berechnet. Da diese Flags aber nur dann aktiv sind, wenn der Preiswert oder das tatsächliche Volumen größer als Null ist, überprüfen wir die Flags nicht, da dies nicht notwendig ist.

HINWEIS: Wenn wir zum Forex kommen, werden wir das ändern. Dies wird jedoch in speziellen Artikeln zum Thema Forex beschrieben werden.

Von nun an wird das Replay-/Simulationssystem das Tick-Volumen anzeigen. Aber es gibt ein Detail: Wenn nun ein Balken zur Simulation von Ticks verwendet wird, weicht das Volumen immer von den Angaben in der Balkendatei ab. Wir werden dies im nächsten Artikel beheben. Dafür ist ein eigener Artikel erforderlich, damit ich in Ruhe erklären kann, was wir tun müssen.


Festlegen eines Bezugspunkts

Das nächste Problem, das gelöst werden muss (obwohl es eigentlich kein Problem ist), besteht darin, dem System mitzuteilen, was die einzelnen Positionseinheiten bedeuten. Das Problem ist, dass dieses System bisher eine sehr ungeeignete Art und Weise verwendet hat, um die vom Nutzer vorgegebene Positionierung durchzuführen. Wenn es dann möglich wird, mehr als eine Datei zu verwenden, um Tickdaten zu erhalten, wird die Situation für das bisherige System völlig inakzeptabel. Daher gibt es Probleme bei der Konvertierung zwischen dem, was in den Kontrollindikator eingegeben wird, und dem, was durch die Wiedergabe erzeugt wird. 

Um dieses Problem zu lösen, müssen Sie eine bestimmte Zeile im Ladesystem entfernen.

                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 traded tick file.");
                                                return false;
                                        }
                                        Print("Loading data for replay. Please 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 = 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);
                        };

Wenn diese Ausnahme ausgelöst wird, wird die Variable „spread“ frei, die ein anderes Mal entsprechend angepasst werden kann. Wir werden dies in diesem Artikel nicht tun, da es noch keinen Bedarf dafür gibt. Aber sobald dies geschehen ist, müssen wir das System, das für die Umwandlung verantwortlich ist, reparieren. Denn von nun an wird das Kontrollsystem der Positionen immer einen ungültigen Punkt anzeigen. Genauer gesagt, wird es der Punkt sein, der sich von dem unterscheidet, was der Nutzer wünscht.

Um die Umwandlung korrekt durchzuführen, müssen wir ein ganz bestimmtes Verfahren ändern. Und zwar so:

                long AdjustPositionReplay(const bool bViewBuider)
                        {
                                u_Interprocess  Info;
                                MqlRates        Rate[def_BarsDiary];
                                int             iPos,
                                                nCount;
                                
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                if (Info.s_Infos.iPosShift == (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks)) return 0;
                                iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));
                                Rate[0].time = macroRemoveSec(m_Ticks.Info[iPos].time);
                                if (iPos < m_ReplayCount)
                                {
                                        CustomRatesDelete(def_SymbolReplay, Rate[0].time, LONG_MAX);
                                        if ((m_dtPrevLoading == 0) && (iPos == 0))
                                        {
                                                m_ReplayCount = 0;
                                                Rate[m_ReplayCount].close = Rate[m_ReplayCount].open = Rate[m_ReplayCount].high = Rate[m_ReplayCount].low = m_Ticks.Info[iPos].last;
                                                Rate[m_ReplayCount].tick_volume = Rate[m_ReplayCount].real_volume = 0;
                                                CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                                        }else
                                        {
                                                for(Rate[0].time -= 60; (m_ReplayCount > 0) && (Rate[0].time <= macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)); m_ReplayCount--);
                                                m_ReplayCount++;
                                        }
                                }else if (iPos > m_ReplayCount)
                                {
                                        if (bViewBuider)
                                        {
                                                Info.s_Infos.isWait = true;
                                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                        }else
                                        {
                                                for(; Rate[0].time > m_Ticks.Info[m_ReplayCount].time; m_ReplayCount++);
                                                for (nCount = 0; m_Ticks.Rate[nCount].time < macroRemoveSec(m_Ticks.Info[iPos].time); nCount++);
                                                CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, nCount);
                                        }
                                }
                                for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag);) Event_OnTime();
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                Info.s_Infos.isWait = false;
                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);

                                return Event_OnTime();
                        }

Die oben dargestellte Umwandlung unterscheidet sich stark von den in früheren Artikeln vorgestellten Versionen. Das liegt daran, dass es die vom Nutzer konfigurierten Prozentwerte in einen Kontrollindikator und ein Positionierungssystem umwandelt, sodass es keine Rolle spielt, wie die Tickets organisiert sind. Dieses Verfahren sucht nach dem richtigen Punkt und beginnt mit der Darstellung der Daten, die in Ticks von diesem Punkt aus gefunden wurden.

Um dies richtig zu tun, führen wir zunächst eine Berechnung durch, um festzustellen, wo prozentual gesehen der gewünschte Punkt liegt. Diese Position ist sehr wichtig. Wenn der Wert niedriger ist, bedeutet das, dass wir zu einem bestimmten Punkt zurückgehen müssen. Wir entfernen dann Informationen, bis wir diesem Punkt näher kommen. Normalerweise werden immer einige zusätzliche Daten entfernt, aber das ist Teil des Prozesses und wir werden diese Daten später zurückgeben. Möglicherweise gehen wir tatsächlich an den Anfang der Datenreihe zurück. Wenn das nicht der Fall ist, setzen wir den Zähler wieder auf einen Punkt zurück, der nahe am Prozentwert liegt. Diese spezielle Zeile behebt das Problem, dass wir immer weiter zurückgehen, als wir eigentlich wollten. Ohne sie ist der Vorschaubalken nicht korrekt. Das Rückwärtssystem ist komplizierter als das Vorwärtssystem. Bei der Vorwärtssuche wird einfach geprüft, ob der Nutzer die erstellten Balken sehen möchte oder nicht. Sie werden auf Wunsch angezeigt, andernfalls springt das System zu dem durch den Prozentwert angegebenen Punkt. In den meisten Fällen müssen wir eine Feinabstimmung zwischen dem Prozentwert und der tatsächlichen Position vornehmen. Allerdings geht das sehr schnell: Wenn der tatsächliche Wert nahe am prozentualen Wert liegt, erfolgt der Übergang praktisch augenblicklich. Befindet sich der Wert jedoch in einiger Entfernung, erscheint eine kleine Animation, die zeigt, wie die Balken aufgebaut werden.


Abschließende Gedanken zu diesem Artikel

Auch wenn das System sehr viel nutzerfreundlicher zu sein scheint, kann es sein, dass Sie einige merkwürdige Dinge bemerken, wenn Sie es im Anzeigemodus der Balkenerstellung betreiben. Diese ungewöhnlichen Dinge sind in dem folgenden Video zu sehen. Da sie jedoch Änderungen an einigen Stellen des Codes erfordern würden und ich nicht wollte, dass Sie denken, diese Dinge kämen aus dem Nichts, habe ich beschlossen, den „Fehler“ zu lassen. Aber vielleicht ist der Hauptgrund, dass ich im nächsten Artikel zeigen werde, wie man das System besser als Simulator einsetzen kann. Ich möchte nicht, dass jemand kommt und mich fragt, warum ich den Simulator programmiert habe, wie im nächsten Artikel gezeigt wird.

Sehen Sie sich jetzt das Video an. Seien Sie sich bitte bewusst, dass ich weiß, was hier geschieht.



Die hier verwendeten Dateien finden Sie im Anhang. Sie erhalten auch eine zusätzliche Datei, die sowohl 1-Minuten-Balken als auch gehandelte Ticks für denselben Tag anzeigt. Führen Sie beide Konfigurationen aus und prüfen Sie die Ergebnisse, aber zuerst müssen Sie verstehen, was im Chart passiert.

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

Beigefügte Dateien |
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.
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.
PrintFormat() studieren und vorgefertigte Beispiele anwenden PrintFormat() studieren und vorgefertigte Beispiele anwenden
Der Artikel ist sowohl für Anfänger als auch für erfahrene Entwickler nützlich. Wir werden uns die Funktion PrintFormat() ansehen, Beispiele für die Formatierung von Zeichenketten analysieren und Vorlagen für die Anzeige verschiedener Informationen im Terminalprotokoll schreiben.
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.