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

Entwicklung eines Replay Systems — Marktsimulation (Teil 22): FOREX (III)

MetaTrader 5Tester | 5 März 2024, 12:34
122 0
Daniel Jose
Daniel Jose

Einführung

Im vorherigen Artikel „Entwicklung eines Replay-Systems — Marktsimulation (Teil 21): FOREX (II)“ haben wir uns vor allem auf die Lösung von Systemproblemen konzentriert, insbesondere auf solche, die die Konfigurationsdatei für die Wiedergabe/Simulation betreffen. Da es hier jedoch bereits eine Menge Informationen für diejenigen gibt, die die Artikel des Systems studieren und verfolgen, um zu lernen, wie sie ihre eigenen Programme erstellen können, haben wir uns entschlossen, den Artikel zu beenden, als das Einrichtungssystem bereits in einem funktionierenden Zustand war, in dem wir für eine recht lange Zeit bequem arbeiten können.

Wenn Sie das Attachment getestet haben, ist Ihnen vielleicht aufgefallen, dass das Replay-/Simulationssystem zwar auf dem Aktienmarkt recht konsistent funktioniert, dasselbe aber nicht über den Devisenmarkt gesagt werden kann. Und damit meine ich nicht nur den Devisenhandel selbst, sondern jeden Vermögenswert, der denselben Preisdarstellungskonzepten folgt wie der Devisenhandel, d. h. das Geld als Basiswert verwendet.

Obwohl dies der dritte Artikel zu diesem Thema ist, muss ich für diejenigen, die den Unterschied zwischen dem Aktienmarkt und dem Devisenmarkt noch nicht verstanden haben, erklären: Der große Unterschied besteht darin, dass es auf dem Devisenmarkt keine Informationen über einige Punkte gibt, die im Laufe des Handels tatsächlich aufgetreten sind. Auch wenn es den Anschein hat, dass dies nur für den Devisenmarkt gilt, meine ich es nicht ausschließlich. Das liegt daran, dass wir auf dem Devisenmarkt unsere eigene Methode haben, um bestimmte Informationen zu erhalten, und das Handelsmodell unterscheidet sich völlig von dem des Aktienmarktes. Ich denke, es ist einfacher, die Unterscheidung auf diese Weise zu treffen. Alles in diesen Artikeln, was sich auf Devisen bezieht, sollte so verstanden werden, dass es für jede Art von Markt gilt, bei dem die Darstellung durch den Geldwert erfolgt, im Gegensatz zu dem, was auf dem Aktienmarkt geschieht, wo der letzte Wert verwendet wird.

Indem wir den Devisenmarkt auf die gleiche Weise abdecken wie den Aktienmarkt (der bereits durch das Replay System des Marktes abgedeckt ist), können wir jede Art von Markt wiedergeben oder simulieren. Und eines der Dinge, die uns im System immer noch Probleme bereiten, ist, dass das Diagramm falsch aussieht, wenn wir die Anzeige der durch die Wiedergabe oder den Simulator erzeugten Balken deaktivieren. Wir sind in der Vergangenheit auf dieses Problem gestoßen, als wir ein System zur Abdeckung des Aktienmarktes entwickelt haben. Da die Anzeige auf dem Devisenmarkt jedoch anders ist und der Grundpreis Bid lautet, kann das derzeitige System dieses Problem nicht richtig lösen. Wenn Sie versuchen, die untersuchte Position zu einem anderen Punkt zu verschieben, während die Balkenanzeige ausgeschaltet ist, werden alle Anzeigen falsch sein, da es keinen Balken zwischen der alten und der neuen Position geben wird. Daher beginnen wir diesen Artikel mit der Behebung dieses Problems.


Korrektur des Schnellortungssystems

Um dieses Problem zu lösen, müssen wir einige Änderungen am Code vornehmen, und zwar nicht, weil er falsch ist, sondern weil er nicht mit Bid-Plotting funktionieren kann. Genauer gesagt besteht das Problem darin, dass es unpraktisch ist, Ticks zu lesen und in 1-Minuten-Balken umzuwandeln (sogar in der Klasse C_FileTicks), um sie anschließend für die schnelle Positionierung zu verwenden. Das liegt daran, dass wir keine Preiswerte auf der Basis von Bid darstellen können. Um zu verstehen, warum das so ist, sehen wir uns den Code an, der für diese Umwandlung verantwortlich ist. Dieser Code ist unten zu sehen:

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);
        m_Ticks.ModePlot = PRICE_FOREX;
        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.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));
            m_Ticks.ModePlot = (def_Ticks.volume_real > 0.0 ? PRICE_EXCHANGE : m_Ticks.ModePlot);
            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
    }

Bitte beachten Sie, dass für den Aufruf der Funktion, die Balken erstellt, ein gehandeltes Volumen vorhanden sein muss. Dieses Volumen tritt nur auf, wenn sich der letzte Preiswert ändert. Bei der Verwendung von Geldkursen ist dieses Volumen immer gleich Null, d.h. das Unterprogramm wird nicht aufgerufen. Als Erstes müssen wir diesen Code hier entfernen, da wir nicht wissen, ob wir beim Lesen von Ticks mit Bid- oder Last-Plotting (letzte Darstellung) arbeiten werden. Die obige Funktion wird also wie folgt geändert:

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

        string   szInfo;
            
        Print("Loading ticks for replay. Please wait...");
        ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
        m_Ticks.ModePlot = PRICE_FOREX;
        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.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));
            m_Ticks.ModePlot = (def_Ticks.volume_real > 0.0 ? PRICE_EXCHANGE : m_Ticks.ModePlot);
            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
    }

Warum tue ich das? Der Grund dafür ist, dass wir erst nach dem Lesen der gesamten Datei wissen, ob die Darstellung anhand des Geldkurses oder des letzten Kurses erfolgen soll. Diese Zeile garantiert nämlich Folgendes. Sie ist jedoch erst gültig, nachdem die Datei gelesen wurde. Was wird passieren, wenn wir die Umwandlung von Ticks in Balken aufrufen, was wird in der Anzeigephase verwendet werden? Im Moment diskutieren wir nicht darüber. Bitte beachten Sie die untenstehende Funktion:

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);
        }else
        {
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TRADE_CALC_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CALC_MODE_EXCH_STOCKS : SYMBOL_CALC_MODE_FOREX);
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_CHART_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CHART_MODE_LAST : SYMBOL_CHART_MODE_BID);
        }
        m_Ticks.bTickReal = true;
        
        return dtRet;
    };

Wir können die Tick-Konvertierungsfunktion nirgendwo in der obigen Funktion aufrufen. Der Grund dafür ist, dass wir an einigen Stellen Ticks in Balken umwandeln müssen und die echte Tick-Datei so verwenden, als wäre sie eine Datei mit früheren 1-Minuten-Balken. Dies ist daran zu erkennen, dass die Funktion CustomRatesUpdate im obigen Code vorhanden ist. Aus diesem Grund müssen wir die Konvertierungsfunktion vor dem Aufruf von CustomRatesUpdate aufrufen. Wenn Sie sich jedoch die Konvertierungsfunktion ansehen, werden Sie feststellen, dass sie für die Verwendung im obigen Code nicht geeignet ist. Die Quellfunktion ist unten dargestellt:

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;
    }

Es ist unpraktisch, diese Funktion dort zu verwenden, wo wir sie brauchen. Daher müssen wir eine weitere Funktion erstellen, um die Konvertierung durchzuführen. Aber wir haben noch ein anderes Problem. Woher wissen wir, wo wir mit der Umstellung beginnen sollen? Denken Sie daran, dass ein Anruf zu verschiedenen Zeiten erfolgen kann und wir jedes Mal andere Bedürfnisse haben können. Bei einem der Anrufe konnten wir Ticks zur Verwendung in der Wiedergabe herunterladen. In einem anderen Aufruf könnten wir Ticks laden, die kurz darauf verworfen werden müssten, weil sie als vorherige Balken verwendet würden. Wie Sie sehen, muss dies sorgfältig durchdacht werden, um nicht in eine Sackgasse zu geraten.

All dies wird viel einfacher und leichter zu bewerkstelligen sein, wenn Sie die erforderlichen Änderungen analysieren und sie genau wie geplant umsetzen. Wir beginnen also mit der Änderung der aufrufenden Prozedur und arbeiten dann an der Konvertierungsfunktion. Die neue Aufrufprozedur ist unten dargestellt:

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;
        BuiderBar1Min(MemNTicks);
        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);
        }else
        {
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TRADE_CALC_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CALC_MODE_EXCH_STOCKS : SYMBOL_CALC_MODE_FOREX);
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_CHART_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CHART_MODE_LAST : SYMBOL_CHART_MODE_BID);
        }
        m_Ticks.bTickReal = true;
        
        return dtRet;
    };

Achten Sie darauf, wie wir den ersten Teil unseres Problems gelöst haben. Mit dieser Zeile stellen wir sicher, dass die Ticks in 1-Minuten-Balken umgewandelt werden, bevor CustomRatesUpdate möglicherweise aufgerufen wird. Gleichzeitig teilen wir der Konvertierungsfunktion mit, wo der Konvertierungsprozess beginnen soll. Wir haben den Aufruf der Konvertierungsfunktion hier und nicht in der Lesefunktion platziert, um zu vermeiden, dass der Lesefunktion eine unnötige Variable hinzugefügt wird. Hier haben wir Zugriff auf die Variable, die wir für die Bestimmung des Bereichs benötigen, in dem wir arbeiten sollen. Jetzt können wir mit der Implementierung der Funktion fortfahren, die Ticks in 1-Minuten-Balken umwandelt. Wir müssen dies auf eine Art und Weise tun, die sowohl für den Forex- als auch für den Devisenmarkt funktioniert. Klingt kompliziert, nicht wahr? Auch hier gilt: Wenn Sie nicht planen, was Sie tun werden, können Sie in einem Kodierungszyklus stecken bleiben, der Sie dazu bringt, den Versuch aufzugeben, den richtigen Code zu implementieren.

Ich möchte Ihnen hier keine Lösung anbieten: Ich möchte, dass Sie lernen, so zu denken, dass Sie eine Lösung finden können, die für Sie funktioniert. Lassen Sie uns nun über Folgendes nachdenken: Das ursprüngliche Konvertierungsverfahren ermöglichte bereits die Konvertierung des letzten Kurses in einen 1-Minuten-Balken. Wir müssen dieser Prozedur eine Schleife hinzufügen, damit sie alle aus der Datei gelesenen Ticks liest. Der Startpunkt dieser Schleife wird uns vom aufrufenden Programm übergeben, und der Endpunkt ist der letzte gelesene Tick. Bis jetzt läuft alles gut. Wir müssen aber auch sicherstellen, wenn das System erkennt, dass wir die Darstellung auf der Grundlage des Bid-Wertes verwenden, dieser Wert den ursprünglich verwendeten Last-Preis ersetzt. Auf diese Weise können wir den Bid-Wert ohne großen Aufwand in einen 1-Minuten-Balken umwandeln. Interessant, nicht wahr? Die Umsetzung dieser Idee wird im Folgenden dargestellt:

inline void BuiderBar1Min(const int iFirst)
    {
        MqlRates rate;
        double   dClose = 0;
        
        rate.time = 0;
        for (int c0 = iFirst; c0 < m_Ticks.nTicks; c0++)
        {
            switch (m_Ticks.ModePlot)
            {
                case PRICE_EXCHANGE:
                    if (m_Ticks.Info[c0].last == 0.0) continue;
                    dClose = m_Ticks.Info[c0].last;
                    break;
                case PRICE_FOREX:
                    dClose = (m_Ticks.Info[c0].bid > 0.0 ? m_Ticks.Info[c0].bid : dClose);
                    if (dClose == 0.0) continue;
                    break;
            }
            if (rate.time != macroRemoveSec(m_Ticks.Info[c0].time))
            {
                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
                rate.time = macroRemoveSec(m_Ticks.Info[c0].time);
                rate.real_volume = 0;
                rate.tick_volume = 0;
                rate.open = rate.low = rate.high = rate.close = dClose;
            }else
            {
                rate.close = dClose;
                rate.high = (rate.close > rate.high ? rate.close : rate.high);
                rate.low = (rate.close < rate.low ? rate.close : rate.low);
                rate.real_volume += (long) m_Ticks.Info[c0].volume_real;
                rate.tick_volume++;
            }
            m_Ticks.Rate[(m_Ticks.nRate += (rate.tick_volume == 0 ? 1 : 0))] = rate;
        }
    }

Die ursprüngliche Funktion ist grün hervorgehoben, damit Sie leichter erkennen können, wo sie vorhanden ist. Diese Codes sind ein neuer Bestandteil der Funktion. Dieser Teil ist ausschließlich für den Austausch von Last-Preisen zu Bid-Preisen oder umgekehrt zuständig, damit der Schlusskurs für die Erstellung eines 1-Minuten-Balkens geeignet ist. In diesem Stadium befindet sich die bereits erwähnte Schleife. Obwohl diese Funktion die meisten unserer Probleme löst, löst sie nicht das Tick-Volumen-Problem, wenn wir die Bid-Darstellung verwenden. Um dieses Problem zu lösen, müssen wir die vorherige Funktion leicht abändern, sodass der endgültige Code wie folgt aussieht:

inline void BuiderBar1Min(const int iFirst)
    {
        MqlRates rate;
        double  dClose = 0;
        bool    bNew;
        
        rate.time = 0;
        for (int c0 = iFirst; c0 < m_Ticks.nTicks; c0++)
        {
            switch (m_Ticks.ModePlot)
            {
                case PRICE_EXCHANGE:
                    if (m_Ticks.Info[c0].last == 0.0) continue;
                    dClose = m_Ticks.Info[c0].last;
                    break;
                case PRICE_FOREX:
                    dClose = (m_Ticks.Info[c0].bid > 0.0 ? m_Ticks.Info[c0].bid : dClose);
                    if ((dClose == 0.0) || (m_Ticks.Info[c0].bid == 0.0)) continue;
                    break;
            }
            if (bNew = (rate.time != macroRemoveSec(m_Ticks.Info[c0].time)))
            {
                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
                rate.time = macroRemoveSec(m_Ticks.Info[c0].time);
                rate.real_volume = 0;
                rate.tick_volume = (m_Ticks.ModePlot == PRICE_FOREX ? 1 : 0);
                rate.open = rate.low = rate.high = rate.close = dClose;
            }else
            {
                rate.close = dClose;
                rate.high = (rate.close > rate.high ? rate.close : rate.high);
                rate.low = (rate.close < rate.low ? rate.close : rate.low);
                rate.real_volume += (long) m_Ticks.Info[c0].volume_real;
                rate.tick_volume++;
            }
            m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate;
        }
    }

In diesem Code, der das Tick-Volumen-Problem tatsächlich löst, mussten wir eine neue Variable hinzufügen. Sein Wert wird in dieser Phase bestimmt, wenn wir bewerten, ob ein neuer Balken hinzugefügt werden soll oder nicht. Diese Variable wird hier nur verwendet, um uns mitzuteilen, ob wir einen neuen Balken in das System einfügen werden oder nicht.

Bitte beachten Sie die folgenden Hinweise: Wenn wir den Modus „Last plotting“ (Darstellung der Last-Preise) verwenden, müssen wir den Zähler von Grund auf neu starten, damit das Tick-Volumen korrekt ist. Bei der Darstellung nach Bid muss der Wert jedoch bei eins beginnen. Andernfalls erhalten wir falsche Daten im Tick-Volumen.

Der Grund liegt in den Details. Aber wenn Sie nicht die richtigen Vorsichtsmaßnahmen treffen, können sie Ihnen schaden.


Korrektur des Tick-Volumens

Man könnte meinen, es gäbe keine Fehler mehr im System. Es gibt jedoch immer noch Unzulänglichkeiten, die behoben werden müssen, und eine davon ist das Mengenproblem. Im vorigen Thema haben wir das Volumen der Ticks korrigiert, die das System im Falle einer schnellen Veränderung der analysierten Position meldet. Wenn Sie jedoch eine Wiedergabe oder Simulation durchführen, ohne die Position schnell zu ändern, sind die Volumeninformationen falsch.

Es geht nicht darum, dass der Code falsch ist, ganz im Gegenteil. Wenn Sie ein System verwenden, um Daten wiederzugeben oder einen Vermögenswert zu simulieren, das den letzten Wert für die Darstellung verwendet, ist das gemeldete Tick-Volumen korrekt. Wenn Sie jedoch den Geldkurs verwenden, wie es bei FOREX der Fall ist, ist dieses Volumen nicht korrekt. Wir müssen dieses Problem nun lösen, damit die Volumenangaben korrekt sind. Um zu verstehen, wo das Problem liegt, sehen wir uns den Code an, der für die Berechnung zuständig ist:

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

        bool    bNew;
        MqlTick tick[1];
        static double PointsPerTick = 0.0;

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

Im Falle eines Vermögenswerts mit einer Bid-basierten Darstellung hat diese Berechnung keine Auswirkung, da diese Art von Volumen im Devisenhandel einfach nicht existiert. Diese Berechnung führt jedoch zu falschen Werten für ein Bid-basiertes Anzeigesystem. Dies ist darauf zurückzuführen, dass in diesen Fällen der Tick keine Informationen über das gehandelte Volumen enthält. Der Wert des Tick-Volumens ist also immer Null.

Bei der Berechnung des Tick-Volumens in der Phase der Erstellung von 1-Minuten-Balken, die bei schnellen Bewegungen verwendet wird, ergibt diese Berechnung jedoch den richtigen Wert. Daher müssen wir den obigen Code korrigieren. Es wird so aussehen:

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

        bool bNew;
        MqlTick tick[1];
        static double PointsPerTick = 0.0;

        if (bNew = (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)))
        {
            PointsPerTick = (PointsPerTick == 0.0 ? SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) : PointsPerTick);            
            m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
            if (m_Ticks.ModePlot == PRICE_FOREX) CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate, (def_Rate.time < m_MountBar.memDT ? 1 : 0));
            def_Rate.real_volume = 0;
            def_Rate.tick_volume = 0;
        }
        def_Rate.close = (m_Ticks.ModePlot == PRICE_EXCHANGE ? (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : def_Rate.close) :
                                                               (m_Ticks.Info[m_ReplayCount].bid > 0.0 ? m_Ticks.Info[m_ReplayCount].bid : 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.ModePlot == PRICE_FOREX) && (m_Ticks.Info[m_ReplayCount].bid > 0.0) ? 1 : (m_Ticks.Info[m_ReplayCount].volume_real > 0 ? 1 : 0));
        def_Rate.time = m_MountBar.memDT;
        CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
        if (bViewTicks)
        {
            tick = m_Ticks.Info[m_ReplayCount];
            if (!m_Ticks.bTickReal)
            {
                static double BID, ASK;
                double dSpread;
                int    iRand = rand();
    
                dSpread = PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? PointsPerTick : 0 ) : 0 );
                if (tick[0].last > ASK)
                {
                    ASK = tick[0].ask = tick[0].last;
                    BID = tick[0].bid = tick[0].last - dSpread;
                }
                if (tick[0].last < BID)
                {
                    ASK = tick[0].ask = tick[0].last + dSpread;
                    BID = tick[0].bid = tick[0].last;
                }
            }
            CustomTicksAdd(def_SymbolReplay, tick); 
        }
        m_ReplayCount++;
        
#undef def_Rate
    }

Fragen Sie mich nicht warum, aber aus irgendeinem seltsamen Grund, von dem ich persönlich keine Ahnung habe, müssen wir diese Zeile hier hinzufügen. Wenn Sie ihn nicht hinzufügen, ist der im Tick-Volumen angezeigte Wert falsch. Achten Sie darauf, dass es eine Bedingung in der Funktion gibt. Dadurch werden Probleme bei der Verwendung des schnellen Positionierungssystems vermieden und es wird verhindert, dass ein seltsamer Balken erscheint, der auf dem Diagramm des Systems außerhalb der Zeit liegt. Obwohl dies ein sehr seltsamer Grund ist, funktioniert alles andere wie erwartet. Dabei handelt es sich um eine neue Berechnung, bei der die Ticks auf die gleiche Weise gezählt werden - sowohl bei der Arbeit mit einem Bid-basierten Asset als auch bei der Arbeit mit einem Last-basierten Instrument.

Wie Sie vielleicht bemerkt haben, ist diese Berechnung recht einfach. Das Interessante daran ist jedoch, dass wir die Kurswerte innerhalb des nutzerdefinierten Assets ein zweites Mal senden müssen. Nachdem die Bar geschlossen wurde. Ich konnte den Grund dafür nicht verstehen. Alles ist so seltsam, dass dieses Senden nur notwendig ist, wenn der Typ Bid verwendet wird, was sehr interessant ist.

Es gibt noch eine weitere Sache, die Ihnen wahrscheinlich bei der vorherigen Funktion aufgefallen ist. Jetzt gibt es das Metriksystem nicht mehr. In gewisser Weise wollte ich dieses System abschaffen, als das Fenster Market Watch um Ticks erweitert wurde. Das liegt daran, dass wir die Zeit, die für die Erstellung der einzelnen Balken benötigt wird, genau abschätzen können. Daher wurde der metrische Code entfernt.


Weichenstellung für die nächste Prüfung

Mit den bisher vorgenommenen Änderungen können wir uns nun der eigentlichen Aufgabe zuwenden: der Erstellung eines Modells für die Ticks des Devisenmarktes, das ausschließlich auf dem Inhalt der 1-Minuten-Balken-Dateien basiert. Glauben Sie mir, die Herausforderung wird ziemlich groß sein, aber gleichzeitig auch sehr interessant und spannend zu bewältigen. Um die Sache zu vereinfachen, werden wir die Klasse C_FileTicks in 2 Klassen aufteilen, aber nur um die Frage zu vereinfachen. Diese Aufteilung ist nicht notwendig, aber da wir mit einigen ziemlich langwierigen Aufgaben zu tun haben werden und ich es nicht mag, wenn Klassen mehr als 1000 Zeilen haben, teilen wir C_FileTicks in zwei Klassen auf.

In diesem Abschnitt werden wir den Teil der Tick-Modellierung aus der Klasse C_FileTicks entfernen. Die Klasse C_Simulation ist für die Konvertierung von 1-Minuten-Balken in Ticks zuständig, damit sie problemlos angezeigt (und ausgeführt) werden können. Die Klasse C_Simulation wird für das Replay System unsichtbar sein. Für den Wiedergabedienst stammen die Daten immer aus der echten Tick-Datei. Sie können sogar aus Simulationen hervorgehen. Selbst wenn Sie versuchen, von der Klasse C_Replay aus auf die Klasse C_Simulation zuzugreifen, ist sie nicht zugänglich. Auf diese Weise wird alles so funktionieren, wie wir es erwarten, da die Klasse C_Replay nur die Klasse C_FileTicks sehen kann, die die tatsächlichen Ticks in der Datei lädt, sodass die Klasse C_Replay sie im MetaTrader 5-Terminal anzeigen kann.

Die neue Deklaration der Klasse C_FileTicks sieht nun wie folgt aus:

#include "C_FileBars.mqh"
#include "C_Simulation.mqh"
//+------------------------------------------------------------------+
#define macroRemoveSec(A) (A - (A % 60))
//+------------------------------------------------------------------+
class C_FileTicks : private C_Simulation
{

// ... Internal class code 

};

Infolgedessen erbt die Klasse C_FileTicks privat von der Klasse C_Simulation. Auf diese Weise erreichen wir genau das, was oben beschrieben wurde.

Wir müssen jedoch kleine Änderungen am Code der Klasse C_FileTicks vornehmen. Das liegt daran, dass wir die Klasse C_Simulation erben. Ich möchte jedoch keine als geschützt deklarierten Daten an die Klasse C_Simulation senden. Dies geschieht, um sicherzustellen, dass die Klasse C_Simulation vor dem Rest des Systems verborgen bleibt. Wir müssen jedoch noch zulassen, dass die von der Klasse geleistete Arbeit von anderen Klassen genutzt werden kann, also müssen wir den folgenden Code ergänzen:

bool BarsToTicks(const string szFileNameCSV)
    {
        C_FileBars  *pFileBars;
        int         iMem = m_Ticks.nTicks,
                    iRet;
        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))
        {
            ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
            m_Ticks.Rate[++m_Ticks.nRate] = rate[0];
            iRet = Simulation(rate[0], local);
            for (int c0 = 0; c0 <= iRet; c0++)
            {
                ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                m_Ticks.Info[m_Ticks.nTicks++] = local[c0];
            }
        }
        ArrayFree(local);
        delete pFileBars;
        m_Ticks.bTickReal = false;
        
        return ((!_StopFlag) && (iMem != m_Ticks.nTicks));
    }

Die hervorgehobenen Zeilen waren Teil des Codes, der von der Prozedur ausgeführt wurde, mit der die Simulation erstellt wurde. Um alles an seinem Platz zu halten, brauchen wir nun eine Funktion, die die Simulation erzeugt und einen Wert zurückgibt. Dieser Wert wird verwendet, um der Klasse C_FileTicks mitzuteilen, wie viele Ticks in einem Array zur späteren Verwendung durch die Klasse C_Replay gespeichert werden sollen.

Wir können uns nun auf die Klasse C_Simulation konzentrieren und ein System erstellen, das eine beliebige Simulationsebene ausschließlich auf der Grundlage der in der 1-Minuten-Balken-Datei enthaltenen Daten durchführt. Diese Frage wird im nächsten Abschnitt dieses Artikels behandelt werden.


Die Klasse C_Simulation

Nachdem wir nun alles aufgeteilt haben und in der Tat mit jeder Art von Markt arbeiten können, müssen wir einen kleinen, aber sehr wichtigen Punkt festlegen. Welche Art von Simulation werden wir erstellen? Wollen wir, dass die Ticks die gleichen sind wie am Aktien- oder Devisenmarkt? Diese Frage scheint Verwirrung zu stiften und zwingt uns dazu, zwei Simulationsmodi zu verwenden. Denken Sie über Folgendes nach: Für Sie (schließlich programmieren Sie das System) ist diese Frage relativ einfach und rein bürokratisch. Aber für den Nutzer ist das etwas verwirrend, und oft will er gar nicht wissen, ob die Datenbank für den Devisen- oder den Aktienmarkt simuliert wird. Sie wollen, dass das System so funktioniert, wie es sollte, d. h. dass es Daten simuliert.

Bei der Arbeit mit 1-Minuten-Balkendateien kann man jedoch auf den ersten Blick nicht erkennen, ob die Daten in dieser Datei von den Devisen- oder Aktienmärkten stammen. Zumindest auf den ersten Blick. Wenn Sie sich diese Dateien jedoch genauer ansehen und die darin enthaltenen Daten vergleichen, können Sie ein bestimmtes Muster erkennen. Dank dieses Musters ist es möglich, die Art des Plottens klar und effizient zu definieren: Gebot oder Last. Zu diesem Zweck müssen wir den Inhalt der Datei mit den Balken einsehen.

Sie als Programmierer sollten immer die Aufgabe übernehmen, das Programm an ein bestimmtes Darstellungsmodell anzupassen. Der Nutzer sollte nicht lernen, welche Art von Modellierung in der 1-Minuten-Balken-Datei vorhanden ist, da der Programmierer das Problem für den Nutzer lösen wird. Wenn Sie nicht verstehen, wovon wir sprechen, machen Sie sich keine Sorgen. Ich weiß, dass dies eine merkwürdige Frage zu sein scheint, aber zu verstehen, wie die Datenbank funktioniert, um dieses spezielle Problem zu lösen, führte mich dazu, den Code des Simulationssystems aufzuteilen. Dies ermöglicht eine bessere Umsetzung. Bevor wir uns der Klasse C_Simulation zuwenden, sollten wir verstehen, wie man ein Plottsystem vom Typ Bid von Last unterscheiden kann.

Ich denke, Sie verstehen, wie das System wissen kann, ob wir eine Wiedergabe von Daten aus dem Forex- oder aus dem Aktienmarkt machen. Wenn Sie das nicht verstehen, empfehle ich Ihnen, die vorherigen Artikel noch einmal zu lesen, bis Sie wirklich verstehen, wie das System eine solche Unterscheidung treffen kann. Aber wenn Sie den Teil über die Wiederholung bereits verstanden haben, lassen Sie uns zu der wichtigen Frage übergehen: Wie kann ein System wissen, ob es mit Forex- oder Börsendaten funktioniert, wenn die einzige Information eine Datei mit 1-Minuten-Balken ist? 

Um herauszufinden, ob wir einen Simulator verwenden sollten, um Ticks zu erstellen und die Information ist, dass in den Devisenmarkt oder den Aktienmarkt verwendet, können wir das Volumen verwenden! Ja, genau, das Volumen! Genau so wird es gemacht. Die Information, die bestimmt, ob eine Bar-Datei zu Forex (wo wir den Bid-Wert als gehandelten Preis verwenden) oder zu einem Aktienmarkt (wo wir Las als gehandelten Preis verwenden) gehört, ist genau das Volumen.

Wenn Sie das nicht verstehen, empfehle ich Ihnen, einen Blick auf die folgenden Bilder zu werfen, auf denen Sie Teile von Dateien mit 1-Minuten-Balken sehen können.


Forex

Abbildung 02 - Datei für Devisenanlagen


Aktienmarkt

Abbildung 03 - Aktiendatei

Uns liegen keine Informationen darüber vor, ob wir den Geldkurs oder den letzten Kurs als Richtwert für den tatsächlichen Handelspreis verwenden. In den obigen Abbildungen ist der einzige Unterschied der Volumenwert. Dieser Wert ist in den Bildern hervorgehoben, damit Sie sehen können, wo der Unterschied liegt.


Abschließende Gedanken zu diesem Artikel

Lassen Sie uns nun einen weiteren Punkt betrachten. In dem Artikel „Entwicklung eines Replay System — Marktsimulation (Teil 11): Geburt des SIMULATORS (I)“, als wir mit der Entwicklung des Simulationssystems begannen, nutzten wir einige der in den 1-Minuten-Balken vorhandenen Ressourcen, um einen Random Walk zu simulieren. Wie in der Datei angegeben, wäre dies die wahrscheinliche Marktbewegung innerhalb dieses 1-Minuten-Balkens. Zu keinem Zeitpunkt der Konstruktion dieses Mechanismus, auch nicht in dem Artikel „Entwicklung eines Replay-Systems — Marktsimulation (Teil 15): Bei der Entwicklung des SIMULATOR (V) - RANDOM WALK“, bei dem wir einen Random Walk konstruiert haben, haben wir Fälle berücksichtigt, in denen das Volumen null oder das Tick-Volumen sehr klein war. Da es aber Märkte und Vermögenswerte gibt, die eine ganz besondere Bedeutung haben, müssen auch solche Fälle abgedeckt werden. Wenn Sie dies nicht tun, bleibt der Dienst irgendwann hängen oder stürzt ab, wenn Sie die Simulation ausführen wollen. Denken Sie daran, dass das Problem im Falle der Simulation auftritt. Im Falle der Wiedergabe wird alles gut funktionieren, solange alles in völliger Harmonie ist, da es keine Notwendigkeit gibt, mit exotischen Situationen zu arbeiten, wie wir es in Zukunft tun werden, wo wir mögliche Marktbewegungen schaffen oder besser gesagt simulieren müssen.

Ich lasse Sie erst einmal darüber nachdenken, was vorher passiert ist. Der Teil, der eine sorgfältige Betrachtung verdient, ist der Teil der Erstellung eines Simulators für einen Forex-Markt, bei dem wir Bid-Werte für das Plotten verwenden. Der Grund dafür ist, dass die Anzahl der Punkte, die erklärt werden müssen, um das System zu verstehen, sehr unterschiedlich zu dem sein wird, was bisher beobachtet wurde. Wir werden einige Teile der Simulationsberechnung neu formulieren müssen. Wir sehen uns im nächsten Artikel.



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

Beigefügte Dateien |
Market_Replay_7vc22.zip (14387.78 KB)
Neuronale Netze leicht gemacht (Teil 60): Online Decision Transformer (ODT) Neuronale Netze leicht gemacht (Teil 60): Online Decision Transformer (ODT)
Die letzten beiden Artikel waren der Decision-Transformer-Methode gewidmet, die Handlungssequenzen im Rahmen eines autoregressiven Modells der gewünschten Belohnungen modelliert. In diesem Artikel werden wir uns einen weiteren Optimierungsalgorithmus für diese Methode ansehen.
Algorithmen zur Optimierung mit Populationen: Stochastische Diffusionssuche (SDS) Algorithmen zur Optimierung mit Populationen: Stochastische Diffusionssuche (SDS)
Der Artikel behandelt die stochastische Diffusionssuche (SDS), einen sehr leistungsfähigen und effizienten Optimierungsalgorithmus, der auf den Prinzipien des Random Walk basiert. Der Algorithmus ermöglicht es, optimale Lösungen in komplexen mehrdimensionalen Räumen zu finden, wobei er sich durch eine hohe Konvergenzgeschwindigkeit und die Fähigkeit auszeichnet, lokale Extrema zu vermeiden.
Neuronale Netze sind einfach (Teil 59): Dichotomy of Control (DoC) Neuronale Netze sind einfach (Teil 59): Dichotomy of Control (DoC)
Im vorigen Artikel haben wir uns mit dem Decision Transformer vertraut gemacht. Das komplexe stochastische Umfeld des Devisenmarktes erlaubte es uns jedoch nicht, das Potenzial der vorgestellten Methode voll auszuschöpfen. In diesem Artikel werde ich einen Algorithmus vorstellen, der die Leistung von Algorithmen in stochastischen Umgebungen verbessern soll.
Entwicklung eines Wiedergabesystems — Marktsimulation (Teil 21): FOREX (II) Entwicklung eines Wiedergabesystems — Marktsimulation (Teil 21): FOREX (II)
Wir werden weiterhin ein System für die Arbeit auf dem FOREX-Markt aufbauen. Um dieses Problem zu lösen, müssen wir zuerst das Laden der Ticks deklarieren, bevor wir die vorherigen Balken laden. Dies löst zwar das Problem, zwingt den Nutzer aber gleichzeitig dazu, sich an eine bestimmte Struktur in der Konfigurationsdatei zu halten, was ich persönlich nicht sehr sinnvoll finde. Der Grund dafür ist, dass wir durch die Entwicklung eines Programms, das für die Analyse und Ausführung der Konfigurationsdatei verantwortlich ist, dem Nutzer die Möglichkeit geben können, die von ihm benötigten Elemente in beliebiger Reihenfolge zu deklarieren.