English Русский 中文 Español 日本語 Português
preview
Entwicklung eines Replay Systems — Marktsimulation (Teil 18): Ticks und noch mehr Ticks (II).

Entwicklung eines Replay Systems — Marktsimulation (Teil 18): Ticks und noch mehr Ticks (II).

MetaTrader 5Tester | 10 Januar 2024, 09:51
243 0
Daniel Jose
Daniel Jose

Einführung

Im vorigen Artikel „Entwicklung eines Wiedergabesystems - Marktsimulation (Teil 17): Ticks und noch mehr Ticks (I)“, haben wir die Möglichkeit hinzugefügt, ein Tick-Chart im System der Marktübersicht anzuzeigen. Dies war eine sehr positive Entwicklung, aber in diesem Artikel erwähnte ich auch, dass unser System einige Mängel aufwies. Daher habe ich beschlossen, einige Funktionen des Dienstes zu deaktivieren, bis die Mängel behoben sind. Jetzt werden wir viele der Fehler beheben, die bei der Anzeige des Tick-Charts aufgetreten sind.

Einer der auffälligsten und für mich vielleicht auch ärgerlichsten Fehler betrifft die Simulationszeit, die für die Erstellung von 1-Minuten-Balken erforderlich ist. Diejenigen, die den Replay-/Simulationsdienst verfolgt und getestet haben, haben vielleicht bemerkt, dass das Timing alles andere als ideal ist. Dies wird noch offensichtlicher, wenn der Vermögenswert eine gewisse Liquidität aufweist, was dazu führen kann, dass wir echte Trades für ein paar Sekunden verlieren. Von Anfang an habe ich versucht, dies zu berücksichtigen und die Nutzung des Replay-/Simulationsdienstes so zu gestalten, dass sie der Erfahrung des Handels mit einem echten Vermögenswert ähnelt.

Offensichtlich sind die aktuellen Metriken sehr weit von der idealen Zeit für die Erstellung eines 1-Minuten-Balkens entfernt. Das ist das erste, was wir in Angriff nehmen werden. Die Behebung des Synchronisationsproblems ist nicht schwierig. Das mag schwierig erscheinen, ist aber eigentlich ganz einfach. Wir haben die erforderliche Korrektur im vorigen Artikel nicht vorgenommen, da er darauf abzielte, zu erklären, wie man die Tick-Daten, die zur Erstellung der 1-Minuten-Balken im Chart verwendet wurden, in das Fenster der Marktübersicht überträgt.

Wenn wir uns entschließen würden, den Timer zu fixieren, wäre es für diejenigen, die wissen wollen, wie die in einer Datei gespeicherten Tick-Daten im Fenster der Marktübersicht angewendet werden, schwer zu verstehen. Wenn wir uns also ausschließlich auf die Aktivierung von Ticks im Fenster der Marktübersicht konzentrieren, ist jetzt klar, wie dieser Prozess abläuft. Ein wichtiges Detail ist, dass ich keine anderen Hinweise darauf gefunden habe, wie man das macht. Die einzige Referenz war die Dokumentation selbst, und bei der Suche fand ich sogar Leute in einem Community-Forum, die ebenfalls wissen wollten, wie man das macht. Sie fanden jedoch keine Antwort, die ihnen wirklich helfen würde zu verstehen, wie der Prozess aussehen sollte. Der vorangegangene Artikel schien mit einer etwas merkwürdigen Bemerkung zu enden, die den Eindruck erweckte, dass nicht klar war, wie die dort erwähnten Probleme zu lösen sind.

Aber hier werden wir uns wirklich damit beschäftigen, wenn auch noch nicht vollständig, denn es gibt Fragen, die viel schwieriger zu erklären sind. Obwohl die Umsetzung oft relativ einfach ist. Es wäre sehr verwirrend, einige Punkte, die völlig unterschiedlich sind, aber irgendwie miteinander zusammenhängen, in einem Artikel zu erklären. Anstatt zu erklären, kann es den gesamten Prozess des Verstehens weiter verkomplizieren.

Meine Idee für jeden Artikel ist es, zu erklären und Menschen zu ermutigen, die MetaTrader 5 Plattform und die MQL5 Sprache zu studieren und zu erforschen. Dies geht weit über das hinaus, was in den irgendwo verteilten Codes zu sehen ist. Ich möchte wirklich, dass jeder von Ihnen kreativ und motiviert ist, Wege zu beschreiten, die noch nie zuvor beschritten wurden, anstatt immer das Gleiche zu tun, als ob MQL5 oder MetaTrader 5 keinen anderen Nutzen brächte als das, was jeder damit macht. Aber zurück zu unserem Artikel.


Implementierung einer Korrektur für die Anfangszeit des 1-Minuten-Balkens

Beginnen wir mit dem Timer. Um das Problem zu beheben, werden wir nur ein kleines Detail im gesamten Code ändern. Diese Änderung ist im nachstehenden Code dargestellt:

inline bool ReadAllsTicks(const bool ToReplay)
                        {
#define def_LIMIT (INT_MAX - 2)
#define def_Ticks m_Ticks.Info[m_Ticks.nTicks]

                                string   szInfo;
                                MqlRates rate;
                                
                                Print("Loading ticks for replay. Please wait...");
                                ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                                while ((!FileIsEnding(m_File)) && (m_Ticks.nTicks < def_LIMIT) && (!_StopFlag))
                                {
                                        ArrayResize(m_Ticks.Info, m_Ticks.nTicks + 1, def_MaxSizeArray);
                                        szInfo = FileReadString(m_File) + " " + FileReadString(m_File);
                                        def_Ticks.time = StringToTime(StringSubstr(szInfo, 0, 19));
                                        def_Ticks.time_msc = (def_Ticks.time * 1000) + (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                                        def_Ticks.time_msc = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                                        def_Ticks.bid = StringToDouble(FileReadString(m_File));
                                        def_Ticks.ask = StringToDouble(FileReadString(m_File));
                                        def_Ticks.last = StringToDouble(FileReadString(m_File));
                                        def_Ticks.volume_real = StringToDouble(FileReadString(m_File));
                                        def_Ticks.flags = (uchar)StringToInteger(FileReadString(m_File));                                       
                                        if (def_Ticks.volume_real > 0.0)
                                        {
                                                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
                                                m_Ticks.nRate += (BuiderBar1Min(rate, def_Ticks) ? 1 : 0);
                                                m_Ticks.Rate[m_Ticks.nRate] = rate;
                                        }
                                        m_Ticks.nTicks++;
                                }
                                FileClose(m_File);
                                if (m_Ticks.nTicks == def_LIMIT)
                                {
                                        Print("Too much data in the tick file.\nCannot continue...");
                                        return false;
                                }
                                return (!_StopFlag);
#undef def_Ticks
#undef def_LIMIT
                        }

Alles ist bereit. Jetzt funktioniert der Timer besser. Sie werden vielleicht denken: Aber wie kann das sein? Das verstehe ich nicht 🤔. Durch einfaches Löschen einer Zeile (sie wurde gelöscht) und Ersetzen durch eine kleine Berechnung ist das Zeitproblem bereits vollständig gelöst, aber es gibt noch mehr als das. Wir könnten auch den Zeitwert entfernen und ihn bei Null belassen.

Dies würde uns mehrere Maschinenzyklen beim Hinzufügen von Ticks zum Chart in der Marktübersicht ersparen. Aber (und dieses „aber“ macht mich wirklich stutzig), wir müssten eine zusätzliche Berechnung durchführen, wenn wir die 1-Minuten-Balken erstellen, die dann in MetaTrader 5 geplottet würden. Dies hätte zur Folge, dass wir allein für die Berechnung mehrere Maschinenzyklen benötigen. Die Art und Weise, wie wir es machen, verursacht viel weniger Kosten.

Mit dieser Änderung können wir sofort eine weitere einführen:

inline void ViewTick(void)
                        {
                                MqlTick tick[1];

                                tick[0] = m_Ticks.Info[m_ReplayCount];
                                tick[0].time_msc = (m_Ticks.Info[m_ReplayCount].time * 1000) + m_Ticks.Info[m_ReplayCount].time_msc;
                                CustomTicksAdd(def_SymbolReplay, tick);
                        }

Wir brauchen die zuvor entfernte Berechnung nicht mehr, da sie in dem Moment durchgeführt wird, in dem wir echte Ticks aus der Datei laden. Hätten wir dies im vorigen Artikel getan, hätten viele nicht verstanden, warum die Ticks auf dem Chart in der Marktübersicht erscheinen. Aber, wie ich schon sagte, scheint mir das jetzt viel klarer geworden zu sein. Allein die Tatsache, dass die Änderungen sanfter ausfallen, macht sie für alle viel verständlicher.

Jetzt kommt eine Frage, die vielleicht ein wenig beunruhigend ist:

Ist es möglich, dass ein Dienst bei einem Vermögenswert mit geringer Liquidität, bei dem der Handel innerhalb weniger Sekunden erfolgen kann, einfrieren kann? Könnte dies verhindern, dass sie geschlossen wird, weil sie dann nicht ganz aufhört? Könnte dies daran liegen, dass der Timer für einige Sekunden auf Standby steht?

Das ist eine gute Frage. Wir werden sehen, warum das nicht passieren wird.

                bool LoopEventOnTime(const bool bViewBuider, const bool bViewMetrics)
                        {

                                u_Interprocess Info;
                                int iPos, iTest;
                                
                                iTest = 0;
                                while ((iTest == 0) && (!_StopFlag))
                                {
                                        iTest = (ChartSymbol(m_IdReplay) != "" ? iTest : -1);
                                        iTest = (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value) ? iTest : -1);
                                        iTest = (iTest == 0 ? (Info.s_Infos.isPlay ? 1 : iTest) : iTest);
                                        if (iTest == 0) Sleep(100);
                                }
                                if ((iTest < 0) || (_StopFlag)) return false;
                                AdjustPositionToReplay(bViewBuider);
                                m_MountBar.delay = 0;
                                while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag))
                                {
                                        CreateBarInReplay(bViewMetrics);
                                        iPos = (int)(m_ReplayCount < m_Ticks.nTicks ? m_Ticks.Info[m_ReplayCount].time_msc - m_Ticks.Info[m_ReplayCount - 1].time_msc : 0);
                                        m_MountBar.delay += (iPos < 0 ? iPos + 1000 : iPos);
                                        if (m_MountBar.delay > 400)
                                        {
                                                if (ChartSymbol(m_IdReplay) == "") break;
                                                GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                                if (!Info.s_Infos.isPlay) return true;
                                                Info.s_Infos.iPosShift = (ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
                                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                                Sleep(m_MountBar.delay - 20);
                                                m_MountBar.delay = 0;
                                        }
                                }                               
                                return (m_ReplayCount == m_Ticks.nTicks);
                        }

Das eigentliche Problem besteht darin, dass der Dienst bei Erreichen der Sleep-Funktion eine Zeit lang „stehen“ bleibt, aber auf keinen Fall beendet werden kann. Sie kann nämlich mit einer STOP-Anforderung gestoppt werden, wenn der Aufruf den Zustand des Stop-Flags ändert. Woher weiß ich das? So steht es in der Dokumentation der Funktion Sleep. Nachstehend finden Sie eine Zusammenfassung, die die Dinge klarstellt.

Hinweis

Die Funktion Sleep() kann nicht von nutzerdefinierten Indikatoren aufgerufen werden, da Indikatoren im Schnittstellen-Thread ausgeführt werden und diesen nicht verlangsamen sollten. Die Funktion hat eine eingebaute Überprüfung des EA-Stopp-Flag-Status alle 0,1 Sekunden.

Daher brauchen wir nicht ständig zu prüfen, ob der Dienst angehalten wurde oder nicht. Die Implementierung von MetaTrader 5 wird dies für uns tun. Das ist sehr gut. Dies erspart uns viel Arbeit bei der Entwicklung einer Möglichkeit, die Funktionalität und gleichzeitig die Interaktivität mit dem Nutzer aufrechtzuerhalten.


Implementierung von Korrekturen im Schnellnavigationssystem

Jetzt werden wir das Problem mit dem Navigationssystem lösen, um alles wieder in den ursprünglichen Zustand zu versetzen. Es gibt einen kleinen Nachteil, den wir mit MQL5 allein nicht lösen konnten. Und da wir nicht versuchen, die Verwendung einer DLL in diesem Stadium zu erzwingen, wird ein kleines Detail in der MetaTrader 5-Plattform verwendet werden müssen. Wir tun dies, um die Dinge in Ordnung zu halten. Was getan werden muss, ist eigentlich ganz einfach und in gewisser Weise sogar dumm. Um jedoch zu verstehen, was getan werden soll, müssen Sie auf das achten, was ich erkläre. Denn auch wenn es auf den ersten Blick fast intuitiv erscheinen mag, können Sie es nicht wirklich verstehen, wenn Sie nicht sehr aufmerksam sind.

Aber wie dem auch sei, schauen wir uns zunächst an, wie der Code geschrieben ist.

#property service
#property icon "\\Images\\Market Replay\\Icon.ico"
#property copyright "Daniel Jose"
#property version   "1.18"
#property description "Replay-simulation system for MT5."
#property description "It is independent from the Market Replay."
#property description "For details see the article:"
#property link "https://www.mql5.com/en/articles/11113"
//+------------------------------------------------------------------+
#define def_Dependence  "\\Indicators\\Market Replay.ex5"
#resource def_Dependence
//+------------------------------------------------------------------+
#include <Market Replay\C_Replay.mqh>
//+------------------------------------------------------------------+
input string            user00 = "Mini Dolar.txt";      //"Replay" config file.
input ENUM_TIMEFRAMES   user01 = PERIOD_M1;             //Initial timeframe for the chart.
input bool              user02 = true;                  //Visual bar construction.
input bool              user03 = true;                  //Visualize creation metrics
//+------------------------------------------------------------------+
void OnStart()
{
        C_Replay        *pReplay;

        pReplay = new C_Replay(user00);
        if ((*pReplay).ViewReplay(user01))
        {
                Print("Permission received. The replay service can now be used...");
                while ((*pReplay).LoopEventOnTime(user02, user03));
        }
        delete pReplay;
}
//+------------------------------------------------------------------+

Jetzt haben wir wieder ein System mit der Möglichkeit, die Visualisierung der Balkenkonstruktion zu aktivieren oder zu deaktivieren. Die Entscheidung liegt jedoch beim Nutzer. Wenn Sie möchten, können Sie diese Visualisierung ausschalten – aus Sicht der Codierung macht das keinen Unterschied. Der Grund dafür ist, dass wir so oder so noch etwas in MetaTrader 5 tun müssen, wenn wir Marktübersicht mit dem Tick-Chart verwenden wollen. Dies ist notwendig, um sicherzustellen, dass das Chart angemessene Werte enthält. Für das reguläre Chart und die Kurslinien sind jedoch keine Änderungen oder Eingriffe erforderlich, da sie korrekt konfiguriert sind. (Das dachte ich, als ich es auf diese Weise machte, aber Sie werden später sehen, dass ich falsch lag. Es gibt einen Fehler, von dem ich nicht wirklich weiß, wie er zu beheben ist, aber das wird in einem anderen Artikel zu sehen sein).

Dazu musste ich einige Änderungen an der Klasse C_Replay vornehmen. Die erste Änderung betraf die Erstellungsroutine für den Balken. Bitte beachten Sie den unten stehenden Code:

inline void CreateBarInReplay(const bool bViewMetrics, const bool bViewTicks)
                        {
#define def_Rate m_MountBar.Rate[0]

                                bool bNew;
                                MqlTick tick[1];

                                if (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time))
                                {                               
                                        if (bViewMetrics) Metrics();
                                        m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                                        def_Rate.real_volume = 0;
                                        def_Rate.tick_volume = 0;
                                }
                                bNew = (def_Rate.tick_volume == 0);
                                def_Rate.close = (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : def_Rate.close);
                                def_Rate.open = (bNew ? def_Rate.close : def_Rate.open);
                                def_Rate.high = (bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high);
                                def_Rate.low = (bNew || (def_Rate.close < def_Rate.low) ? def_Rate.close : def_Rate.low);
                                def_Rate.real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real;
                                def_Rate.tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > 0 ? 1 : 0);
                                def_Rate.time = m_MountBar.memDT;
                                CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
                                tick = m_Ticks.Info[m_ReplayCount];
                                if (bViewTicks) CustomTicksAdd(def_SymbolReplay, tick);
                                m_ReplayCount++;
                                
#undef def_Rate
                        }

Diese Routine war erforderlich, um ein neues Argument hinzuzufügen. Mit diesem Argument können Sie das Senden von Ticks an das Fenster er Marktübersicht aktivieren oder deaktivieren. Warum das? Das liegt daran, dass es nicht möglich ist, den Tick-Chart in der Marktübersicht und den Balken-Chart gleichzeitig zu aktualisieren. Dies geschieht, wenn wir den Dienst so konfigurieren, dass er jederzeit gestartet werden kann. Bei normaler Verwendung können wir jedoch ohne Probleme Ticks sowohl an das Fenster der Marktübersicht als auch an das Balkenchart senden, was recht merkwürdig ist.

Dann werden Sie vielleicht denken: Wir können also den Inhalt des Fensters der Market nicht wirklich ändern. Wir können, aber nicht auf jeden Fall. Alles, was wir wirklich tun können und werden, ist alte Ticks zu entfernen. Dies wird jedoch nicht ohne weiteres möglich sein, zumindest solange die Entwickler der MetaTrader 5-Plattform das Problem im Zusammenhang mit der Verwendung von nutzerdefinierten Symbolen im Fenster der Marktübersicht nicht gelöst haben. Das liegt daran, dass die im nutzerdefinierten Symbol platzierten Ticks nicht aus dem Fenster verschwinden, in dem die nutzerdefinierten Ticks zu sehen sind. Seltsamerweise bleiben sie dort, was es schwer verständlich macht, wenn wir zu einer früheren Position zurückkehren.

In jedem Fall sehen Sie unten die Funktion, die für die Verwaltung des Positionssystems zuständig ist:

                void AdjustPositionToReplay(const bool bViewBuider)
                        {
                                u_Interprocess Info;
                                MqlRates       Rate[def_BarsDiary];
                                int            iPos, nCount;
                                
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                if (m_ReplayCount == 0)
                                        for (; m_Ticks.Info[m_ReplayCount].volume_real == 0; m_ReplayCount++);
                                if (Info.s_Infos.iPosShift == (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks)) return;
                                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);
                                        CustomTicksDelete(def_SymbolReplay, m_Ticks.Info[iPos].time_msc, LONG_MAX);
                                        if ((m_dtPrevLoading == 0) && (iPos == 0)) FirstBarNULL(); 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);) CreateBarInReplay(false, false);
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                Info.s_Infos.isWait = false;
                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                        }

Dies ist die einzige Änderung gegenüber der Vorgängerversion: eine Funktion, die Ticks von einem bestimmten Punkt im Tick-Chart des Fensters der Marktübersicht entfernt. Warum haben wir diese Umsetzung nicht schon früher gesehen? Denn ich habe immer noch versucht, eine dynamische Datenaktualisierung für beide Charts (mit Balken und mit Ticks) zu erreichen. Aber ich konnte es nicht tun, ohne Fehler und Probleme mit dem Aktualisierungssystem zu verursachen. Irgendwann habe ich einfach beschlossen, dass nur das Balkenchart aktualisiert werden soll, und deshalb hat diese Funktion jetzt 2 Parameter. 

Nun, da das System fast dasselbe ist wie vor der Einführung der Tick-Chart-Anzeige und der Verwendung des Fensters der Marktübersicht, möchte ich noch eine Sache zeigen, bevor ich diesen Artikel beende. Aber ich hoffe, Sie verstehen, wie die Wiedergabe/Modellierung funktioniert, wenn wir mit echten Daten arbeiten. Jetzt fügen wir dem Fenster der Marktübersicht nur dann Ticks hinzu, wenn die Daten simuliert wurden. Und genau das ist das Thema des nächsten Abschnitts.


Verwendung simulierter Daten in der Marktübersicht

Da ich dieses Thema über Ticks im Fenster der Marktübersicht nicht auf einen weiteren Artikel ausdehnen möchte, sehen wir uns an, wie man es macht, oder besser gesagt, wie mein Vorschlag für diese Art von Situation aussieht. Was die Modellierung von Ticks von 1-Minuten-Balken betrifft, so ist die Frage hier viel einfacher als das, was bisher getan wurde. Wenn Sie alles verstanden haben, was vorher war, dann werden Sie auch keine Probleme haben, dies zu verstehen.

Anders als bei der Verwendung von realen Handelsdaten fehlen uns bei der Verwendung von simulierten Daten zunächst einige Arten von Informationen. Das bedeutet nicht, dass es unmöglich ist, diese Daten zu erstellen, sondern dass dies sorgfältig geschehen muss. Die Information, von der ich spreche, ist, wenn sich der Preis der Leiste über den Bereich zwischen BID und ASK hinaus bewegt. Wenn Sie sich die Wiederholung genau ansehen, werden Sie feststellen, dass dieser Durchbruch durch den von BID und ASK begrenzten Bereich immer sehr schnell und selten ist. Und nach meiner Erfahrung auf dem Markt passieren diese Dinge immer dann, wenn die Preisvolatilität in die Höhe schnellt. Aber wie gesagt, das sind seltene Ereignisse.

HINWEIS: Glauben Sie also nicht, dass Sie sich immer innerhalb der Spanne bewegen können und werden. Manchmal kann das System über den Spread hinausgehen. Es ist wichtig, dass Sie dies wissen, denn bei der Entwicklung eines Auftragssystems werden diese Informationen und ihr richtiges Verständnis von entscheidender Bedeutung sein. 

Wichtig: Der Preis setzt sich zwar aus BID und ASK zusammen, aber das bedeutet nicht, dass es eine Lücke oder einen Zusammenbruch im System gibt. Wir haben das Update des Handelsservers mit den neuen BID- und ASK-Werten einfach nicht rechtzeitig erhalten. Aber wenn Sie das Orderbuch verfolgen, werden Sie sehen, dass die Dinge ein wenig anders liegen, als viele sich das vorstellen. Deshalb muss man viel Erfahrung mit dem gesamten Handelssystem haben, um die Probleme, die es gibt, wirklich zu kennen.

Wenn Sie dies wissen, können Sie sogar in Erwägung ziehen, solche Bewegungen in Ihr Simulationssystem einzubauen. Dadurch wird die Situation noch realistischer. Aber bedenken Sie, dass solche Dinge mit Vorsicht zu genießen sind. Idealerweise sollten Sie die Anlage, für die diese Art von Bewegung simuliert werden soll, sehr gut kennen. Nur auf diese Weise können Sie das Geschehen näher an das heranführen, was auf einem realen Markt tatsächlich passieren würde. Um zu verstehen, wie diese Art von Bewegung im Simulator aktiviert werden kann, sollten wir uns zunächst ansehen, wie der Simulator implementiert werden sollte, damit der Preis immer innerhalb der Spanne zwischen BID und ASK bleibt.

Zunächst müssen wir eine neue Variable hinzufügen.

struct st00
        {
                MqlTick  Info[];
                MqlRates Rate[];
                int      nTicks,
                         nRate;
                bool     bTickReal;
        }m_Ticks;

Damit können wir feststellen, ob die Ticks auf echten oder simulierten Daten beruhen. Dieser Unterschied ist von entscheidender Bedeutung, da wir die BID- und ASK-Bewegungen nicht wirklich simulieren werden. Wir werden diese Limits auf der Grundlage des Wertes des letzten vom Simulator generierten Handelspreises erstellen. Der Hauptgrund ist jedoch, dass die BID- und ASK-Werte nicht am gemeldeten Handelsvolumen beteiligt sind. Um die Simulationsfunktion einfach zu halten, werden wir diese Einstellung vornehmen, um BID und ASK an anderer Stelle zu erzeugen.

Sobald wir eine neue Variable haben, müssen wir sie richtig initialisieren. Es gibt zwei Stellen, an denen er initialisiert wird. Die erste ist, dass wir angeben, dass wir mit simulierten Ticks arbeiten.

                bool BarsToTicks(const string szFileNameCSV)
                        {
                                C_FileBars      *pFileBars;
                                int             iMem = m_Ticks.nTicks;
                                MqlRates        rate[1];
                                MqlTick         local[];
                                
                                pFileBars = new C_FileBars(szFileNameCSV);
                                ArrayResize(local, def_MaxSizeArray);
                                Print("Converting bars to ticks. Please wait...");
                                while ((*pFileBars).ReadBar(rate) && (!_StopFlag)) Simulation(rate[0], local);
                                ArrayFree(local);
                                delete pFileBars;
                                m_Ticks.bTickReal = false;
                                
                                return ((!_StopFlag) && (iMem != m_Ticks.nTicks));
                        }

Eine weitere Stelle, an der wir diese Variable initialisieren, ist, wenn wir angeben, dass wir mit echten Ticks arbeiten.

                datetime LoadTicks(const string szFileNameCSV, const bool ToReplay = true)
                        {
                                int      MemNRates,
                                         MemNTicks;
                                datetime dtRet = TimeCurrent();
                                MqlRates RatesLocal[];
                                
                                MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
                                MemNTicks = m_Ticks.nTicks;
                                if (!Open(szFileNameCSV)) return 0;
                                if (!ReadAllsTicks(ToReplay)) return 0;
                                if (!ToReplay)
                                {
                                        ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates));
                                        ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0);
                                        CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates));
                                        dtRet = m_Ticks.Rate[m_Ticks.nRate].time;
                                        m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates);
                                        m_Ticks.nTicks = MemNTicks;
                                        ArrayFree(RatesLocal);
                                }
                                m_Ticks.bTickReal = true;
                                                                        
                                return dtRet;
                        };

Da wir nun wissen, ob wir es mit echten oder simulierten Ticks zu tun haben, können wir uns an die Arbeit machen. Doch bevor wir uns der Klasse C_Replay zuwenden und mit der Konfiguration beginnen, müssen wir einige kleine Änderungen am Simulator selbst vornehmen. Wenn wir echte Ticks laden, passen wir die Zeit so an, dass der Wert im Feld Millisekunden so korrigiert wird, dass er einen bestimmten Zeitpunkt darstellt. Der Simulator nimmt diese Anpassung jedoch noch nicht vor. Wenn wir also versuchen, das System auch nach der Änderung der Klasse C_Replay laufen zu lassen, werden wir keine wirkliche Vorstellung von den simulierten Daten bekommen. Der Grund dafür ist, dass die im Feld Millisekunden angegebene Zeit nicht korrekt ist.

Um dieses Problem zu beheben, werden wir die folgenden Änderungen vornehmen:

inline void Simulation(const MqlRates &rate, MqlTick &tick[])
                        {
#define macroRandomLimits(A, B) (int)(MathMin(A, B) + (((rand() & 32767) / 32767.0) * MathAbs(B - A)))

                                long     il0, max, i0, i1;
                                bool     b1 = ((rand() & 1) == 1);
                                double   v0, v1;
                                MqlRates rLocal;
                                
                                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
                                m_Ticks.Rate[++m_Ticks.nRate] = rate;
                                max = rate.tick_volume - 1;     
                                v0 = 4.0;
                                v1 = (60000 - v0) / (max + 1.0);
                                for (int c0 = 0; c0 <= max; c0++, v0 += v1)
                                {
                                        tick[c0].last = 0;
                                        tick[c0].flags = 0;
                                        il0 = (long)v0;
                                        tick[c0].time = rate.time + (datetime) (il0 / 1000);
                                        tick[c0].time_msc = (tick[c0].time * 1000) + (il0 % 1000);
                                        tick[c0].time_msc = il0 % 1000;
                                        tick[c0].volume_real = 1.0;
                                }
                                tick[0].last = rate.open;
                                tick[max].last = rate.close;
                                for (int c0 = (int)(rate.real_volume - rate.tick_volume); c0 > 0; c0--)
                                        tick[macroRandomLimits(0, max)].volume_real += 1.0;                                     
                                i0 = (long)(MathMin(max / 3.0, max * 0.2));
                                i1 = max - i0;
                                rLocal = rate;  
                                rLocal.open = rate.open;
                                rLocal.close = (b1 ? rate.high : rate.low);
                                i0 = RandomWalk(1, i0, rLocal, tick, 0);
                                rLocal.open = tick[i0].last;
                                rLocal.close = (b1 ? rate.low : rate.high);
                                RandomWalk(i0, i1, rLocal, tick, 1);
                                rLocal.open = tick[i1].last;
                                rLocal.close = rate.close;
                                RandomWalk(i1, max, rLocal, tick, 2);
                                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];
                                }
#undef macroRandomLimits
                        }

Wir entfernen einige Codes und ersetzen sie durch den empfohlenen Code. Auf diese Weise wird die Zeit in Millisekunden mit den Erwartungen der Klasse C_Replay kompatibel sein. Jetzt können wir die Änderungen vornehmen, um den simulierten Inhalt anzuzeigen.

In der Klasse C_Replay werden wir uns darauf konzentrieren, Änderungen an einer einzelnen Funktion vorzunehmen, die im folgenden Code gezeigt wird:

inline void CreateBarInReplay(const bool bViewMetrics, const bool bViewTicks)
                        {
#define def_Rate m_MountBar.Rate[0]

                                bool bNew;
                                MqlTick tick[1];

                                if (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time))
                                {                               
                                        if (bViewMetrics) Metrics();
                                        m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                                        def_Rate.real_volume = 0;
                                        def_Rate.tick_volume = 0;
                                }
                                bNew = (def_Rate.tick_volume == 0);
                                def_Rate.close = (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : def_Rate.close);
                                def_Rate.open = (bNew ? def_Rate.close : def_Rate.open);
                                def_Rate.high = (bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high);
                                def_Rate.low = (bNew || (def_Rate.close < def_Rate.low) ? def_Rate.close : def_Rate.low);
                                def_Rate.real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real;
                                def_Rate.tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > 0 ? 1 : 0);
                                def_Rate.time = m_MountBar.memDT;
                                CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
                                if (bViewTicks)
                                {
                                        tick = m_Ticks.Info[m_ReplayCount];
                                        if (!m_Ticks.bTickReal)
                                        {
                                                tick[0].bid = tick[0].last - m_PointsPerTick;
                                                tick[0].ask = tick[0].last + m_PointsPerTick;
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }
                                m_ReplayCount++;
                                
#undef def_Rate
                        }

Wir haben hier eine ziemlich einfache Änderung. Diese Änderung ist nur für die Erstellung und Darstellung von BID- und ASK-Werten vorgesehen. Beachten Sie jedoch, dass diese Erstellung auf dem Wert des letzten Geschäftspreises basiert und nur dann erfolgt, wenn wir mit simulierten Werten arbeiten. Wenn wir diesen Code ausführen, erhalten wir eine interne Darstellung des Charts, der vom RANDOM WALK-System erzeugt wurde, genauso wie wir es mit EXCEL gemacht haben. Wenn, in dem Artikel „Entwicklung eines Replay-Systems - Marktsimulation (Teil 15): Geburt des SIMULATORS (V) - RANDOM WALK“, habe ich erwähnt, dass es andere Möglichkeiten gibt, die gleiche Visualisierung zu machen, ich habe mich auf dieses Modell bezogen. Aber es war nicht der richtige Zeitpunkt, um zu sagen, wie man es macht. Jetzt ist es an der Zeit.

Wenn diese BID- und ASK-Werte nicht erstellt werden, können wir den Wert nur auf der Grundlage des letzten simulierten Preises anzeigen. Das mag für Sie ausreichend sein, aber manche Leute sehen sich wirklich gerne den BID- und ASK-Wert an. Die Verwendung von Daten auf diese Weise ist jedoch nicht ganz angemessen. Die Tatsache, dass Operationen genau innerhalb der BID und ASK durchgeführt werden, ohne sie tatsächlich zu berühren, deutet darauf hin, dass direkte Operationen auf dem Markt durchgeführt werden. Die Geschäfte werden also ohne das Orderbuch abgewickelt. In diesem Fall sollte sich der Kurs nicht bewegen, was er aber tut, wie wir im Simulator sehen werden. Daher müssen wir nur den grün markierten Teil korrigieren. Dies geschieht, damit die Bewegung zumindest dem entspricht, was man eigentlich erwarten würde.

Sehen Sie sich das hervorgehobene Segment und die unten dargestellten Änderungen an:

                                if (bViewTicks)
                                {
                                        tick = m_Ticks.Info[m_ReplayCount];
                                        if (!m_Ticks.bTickReal)
                                        {
                                                static double BID, ASK;
                                                
                                                if (tick[0].last > ASK)
                                                {
                                                        ASK = tick[0].ask = tick[0].last;
                                                        BID = tick[0].bid = tick[0].last - m_PointsPerTick;
                                                }
                                                if (tick[0].last < BID)
                                                {
                                                        ASK = tick[0].ask = tick[0].last + m_PointsPerTick;
                                                        BID = tick[0].bid = tick[0].last;
                                                }
                                                tick[0].ask = tick[0].last + m_PointsPerTick;
                                                tick[0].bid = tick[0].last - m_PointsPerTick;
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }

Wir haben einen Teil des Codes entfernt und ein paar weitere Punkte hinzugefügt. Es ist zu beachten, dass die BID- und ASK-Werte statisch sind, was notwendig ist, um einen kleinen Indikator erstellen zu können. Etwas sehr Einfaches, aber genug, um eine Revolution auszulösen. Da es sehr wahrscheinlich ist, dass diese Werte beim Systemstart Null sind, werden wir zunächst einen Aufruf haben, bei dem der Schwerpunkt auf ASK liegt, und wir haben einen ziemlich engen Kanal von nur 1 Tick. Bis der letzte Angebotspreis diesen Kanal verlässt, wird er dort bleiben.

Das ist einfach, aber funktionell. Achten Sie darauf, dass der BID-Wert nicht mit dem ASK-Wert kollidieren darf (dies gilt für bestimmte Handelsmärkte, auf dem Forex-Markt sieht es anders aus, aber dazu später mehr). Die Ursache für die Kollision ist der Wert des zuletzt getätigten Geschäfts. Was passiert, wenn wir eine kleine Änderung am oben gezeigten Code vornehmen? Etwas sehr Subtiles. Was passiert, wenn sich der BID- oder ASK-Wert ändert, ohne dass sich der Preis des letzten Geschäfts ändert?

Ändern wir den Code wie folgt:

                                if (bViewTicks)
                                {
                                        tick = m_Ticks.Info[m_ReplayCount];
                                        if (!m_Ticks.bTickReal)
                                        {
                                                static double BID, ASK;
                                                
                                                if (tick[0].last > ASK)
                                                {
                                                        ASK = tick[0].ask = tick[0].last;
                                                        BID = tick[0].bid = tick[0].last - (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                }
                                                if (tick[0].last < BID)
                                                {
                                                        ASK = tick[0].ask = tick[0].last + (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                        BID = tick[0].bid = tick[0].last;
                                                }
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }

Sehen Sie, wie interessant das ist. Indem wir dem System ein gewisses Maß an Zufälligkeit hinzugefügt haben, haben wir die Einbeziehung von Direktaufträgen ermöglicht. Das heißt, der Aufträge, die ohne Änderung des BID oder ASK ausgeführt werden. Auf dem realen Markt kommen solche Aufträge nicht sehr häufig vor, und auch nicht in der Form, in der sie vom System angezeigt werden. Aber wenn wir diese Tatsache ignorieren, haben wir bereits ein gutes System, in dem manchmal eine kleine Spanne zwischen BID und ASK enthalten ist. Mit anderen Worten: Der Simulator passt sich praktisch an eine viel häufigere Situation auf dem realen Markt an. Wir sollten uns jedoch vor einem Übermaß an direkten Anweisungen hüten. Um diesen Exzess zu vermeiden, können wir die Dinge etwas weniger zufällig gestalten.

Zu diesem Zweck müssen wir die letzte Änderung vornehmen. Es ist unten aufgeführt:

                                if (bViewTicks)
                                {
                                        tick = m_Ticks.Info[m_ReplayCount];
                                        if (!m_Ticks.bTickReal)
                                        {
                                                static double BID, ASK;
                                                double  dSpread;
                                                int     iRand = rand();
                                                
                                                dSpread = m_PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_PointsPerTick : 0 ) : 0 );
                                                if (tick[0].last > ASK)
                                                {
                                                        ASK = tick[0].ask = tick[0].last;
                                                        BID = tick[0].bid = tick[0].last - dSpread;
                                                        BID = tick[0].bid = tick[0].last - (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                }
                                                if (tick[0].last < BID)
                                                {
                                                        ASK = tick[0].ask = tick[0].last + (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                        ASK = tick[0].ask = tick[0].last + dSpread;
                                                        BID = tick[0].bid = tick[0].last;
                                                }
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }

Hier kontrollieren wir den Grad der Komplexität der Zufallsgenerierung. Dies geschieht, um einen gewissen Grad an Kontingenz zu wahren. Wir werden von Zeit zu Zeit Direktaufträge erteilen. Dies wird jedoch in kontrollierteren Mengen geschehen. Zu diesem Zweck werden wir diese Werte hier einfach anpassen. Durch ihre Anpassung schaffen wir ein kleines Fenster, in dem die Spanne wahrscheinlich etwas größer ist als der kleinstmögliche Wert. Daher werden wir uns von Zeit zu Zeit mit direkten Aufträgen befassen, die durch das Modellierungssystem generiert werden, was in früheren Artikeln oder bis jetzt nicht möglich war.


Schlussfolgerung

In diesem Artikel habe ich Ihnen gezeigt, wie Sie das System zum Setzen und Erstellen von Ticks in einem Chart mit der Marktübersicht implementieren können. Damit haben wir im letzten Artikel begonnen. Als Ergebnis haben wir ein Simulationssystem entwickelt, das sogar direkte Aufträge simulieren kann. Dies war nicht Teil unserer ursprünglichen Ziele. Wir sind noch weit davon entfernt, dass dies in einigen Arten von Handelssystemen vollständig genutzt werden kann. Aber was wir heute getan haben, ist erst der Anfang.

Im nächsten Artikel setzen wir unsere Serie über die Erstellung des Marktwiedergabe-/Simulationssystems fort. Der Anhang enthält 4 verschiedene Ressourcen zum Testen und Überprüfen des Systembetriebs. Denken Sie daran, dass ich sowohl echte Tickdaten als auch 1-Minuten-Balken zur Verfügung stellen werde, damit Sie den Unterschied zwischen den simulierten und den echten Werten sehen können. So können Sie anfangen, die Dinge tiefer zu analysieren. Um alles zu verstehen, was hier erklärt wird, müssen Sie den Wiedergabe-/Simulationsdienst in beiden Modi ausführen. Überprüfen Sie zunächst das nutzerdefinierte Symbol bei der Ausführung der Simulation, und sehen Sie sich dann an, was es bei der Wiedergabe bewirkt. Achten Sie aber auf das Tick-Fenster, nicht auf das Chart selbst. Sie werden sehen, dass der Unterschied wirklich spürbar ist. Zumindest was den Inhalt des Marktbeobachtungsfensters betrifft.


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

Beigefügte Dateien |
Market_Replay_rvt_18.zip (12899.62 KB)
Entwicklung eines Replay Systems — Marktsimulation (Teil 19): Erforderliche Anpassungen Entwicklung eines Replay Systems — Marktsimulation (Teil 19): Erforderliche Anpassungen
Hier werden wir den Boden bereiten, damit wir, wenn wir neue Funktionen zum Code hinzufügen müssen, dies reibungslos und einfach tun können. Der derzeitige Kodex kann einige der Dinge, die notwendig sind, um sinnvolle Fortschritte zu erzielen, noch nicht abdecken oder behandeln. Wir müssen alles strukturieren, damit wir bestimmte Dinge mit minimalem Aufwand umsetzen können. Wenn wir alles richtig machen, erhalten wir ein wirklich universelles System, das sich sehr leicht an jede Situation anpassen lässt, die es zu bewältigen gilt.
Neuronale Netze leicht gemacht (Teil 53): Aufteilung der Belohnung Neuronale Netze leicht gemacht (Teil 53): Aufteilung der Belohnung
Wir haben bereits mehrfach darüber gesprochen, wie wichtig die richtige Wahl der Belohnungsfunktion ist, mit der wir das gewünschte Verhalten des Agenten anregen, indem wir Belohnungen oder Bestrafungen für einzelne Aktionen hinzufügen. Aber die Frage nach der Entschlüsselung unserer Signale durch den Agenten bleibt offen. In diesem Artikel geht es um die Aufteilung der Belohnung im Sinne der Übertragung einzelner Signale an den trainierten Agenten.
Neuronale Netze leicht gemacht (Teil 52): Forschung mit Optimismus und Verteilungskorrektur Neuronale Netze leicht gemacht (Teil 52): Forschung mit Optimismus und Verteilungskorrektur
Da das Modell auf der Grundlage des Erfahrungswiedergabepuffers trainiert wird, entfernt sich die aktuelle Strategie oder Politik des Akteurs immer weiter von den gespeicherten Beispielen, was die Effizienz des Trainings des Modells insgesamt verringert. In diesem Artikel befassen wir uns mit einem Algorithmus zur Verbesserung der Effizienz bei der Verwendung von Stichproben in Algorithmen des verstärkten Lernens.
Entwicklung eines Replay Systems — Marktsimulation (Teil 17): Ticks und noch mehr Ticks (I) Entwicklung eines Replay Systems — Marktsimulation (Teil 17): Ticks und noch mehr Ticks (I)
Hier werden wir sehen, wie man etwas wirklich Interessantes, aber gleichzeitig auch sehr Schwieriges umsetzen kann, da bestimmte Punkte sehr verwirrend sein können. Das Schlimmste, was passieren kann, ist, dass einige Händler, die sich für Profis halten, nichts über die Bedeutung dieser Konzepte auf dem Kapitalmarkt wissen. Auch wenn wir uns hier auf die Programmierung konzentrieren, ist das Verständnis einiger der Probleme, die mit dem Markthandel verbunden sind, von entscheidender Bedeutung für das, was wir umsetzen werden.