English Русский 中文 Español 日本語 Português
preview
Entwicklung eines Wiedergabesystems — Marktsimulation (Teil 04): Anpassung der Einstellungen (II)

Entwicklung eines Wiedergabesystems — Marktsimulation (Teil 04): Anpassung der Einstellungen (II)

MetaTrader 5Beispiele | 21 August 2023, 08:31
215 0
Daniel Jose
Daniel Jose

Einführung

Im vorangegangenen Artikel „Entwicklung eines Replay-Systems — Marktsimulation (Teil 03): Anpassen der Einstellungen (I)“ haben wir einen EA erstellt, der den Market Replay Service (Marktwiedergabeservice) einfach verwalten kann. Bisher ist es uns gelungen, einen wichtigen Punkt zu implementieren: das System zu pausieren und abzuspielen. Wir haben keine Steuerung entwickelt, mit der Sie die gewünschte Startposition für die Wiedergabe auswählen können. Das heißt, es ist noch nicht möglich, die Wiedergabe von der Mitte des Zeitraums oder von einem anderen bestimmten Punkt aus zu starten. Wir müssen immer am Anfang der Daten beginnen, was für diejenigen, die eine Ausbildung machen wollen, nicht praktisch ist.

Dieses Mal werden wir die Möglichkeit, den Startpunkt der Wiedergabe auszuwählen, auf die einfachste Art und Weise umsetzen. Wir werden auch eine kleine Änderung in der Strategie auf Wunsch einiger Freunde vornehmen, die dieses System mochten und es gerne in ihren eigenen EAs verwenden würden. Wir werden also die entsprechenden Änderungen am System vornehmen, um dies zu ermöglichen.

Auf diese Weise wollen wir zeigen, wie eine neue Anwendung tatsächlich erstellt wird. Viele Leute denken, dass dies aus dem Nichts kommt und verstehen nicht den gesamten Prozess, von der Idee bis zur vollständigen Stabilisierung des Systems und des Codes, der es der Anwendung ermöglicht, genau das zu tun, was wir erwarten.


Den EA gegen den Indikator austauschen

Diese Änderung ist relativ einfach zu implementieren. Danach werden wir in der Lage sein, unseren eigenen EA zu verwenden, um mit dem Market Replay Service zu recherchieren oder auf dem Live-Markt zu handeln. Zum Beispiel können wir den EA verwenden, den ich in früheren Artikeln gezeigt habe. Lesen Sie mehr in der Serie „Entwicklung eines Expert Advisor für den Handel von Grund auf“. Er ist zwar nicht für eine 100%ige Automatisierung ausgelegt, kann aber für die Verwendung in einem Wiedergabedienst angepasst werden. Aber lassen wir das für die Zukunft. Darüber hinaus können wir auch einige EAs aus der Serie „Erstellen eines EA, der automatisch funktioniert (Teil 01): Konzepte und Strukturen“, wo wir uns angesehen haben, wie man einen EA Advisor erstellt, der vollautomatisch arbeitet.

Unser derzeitiger Schwerpunkt liegt jedoch nicht auf dem EA (wir werden dies in Zukunft untersuchen), sondern auf etwas anderem.

Der vollständige Code des Indikators ist unten zu sehen. Er enthält genau die Funktionsweisen, die bereits im EA vorhanden war, und implementiert sie als Indikator:

#property copyright "Daniel Jose"
#property indicator_chart_window
#property indicator_plots 0
//+------------------------------------------------------------------+
#include <Market Replay\C_Controls.mqh>
//+------------------------------------------------------------------+
C_Controls      Control;
//+------------------------------------------------------------------+
int OnInit()
{
        IndicatorSetString(INDICATOR_SHORTNAME, "Market Replay");
        Control.Init();
        
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
{
        return rates_total;
}
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        Control.DispatchMessage(id, lparam, dparam, sparam);
}
//+------------------------------------------------------------------+


Der einzige Unterschied besteht in der Hinzufügung einer Kurzbezeichnung, die vorzugsweise in den Indikatoren enthalten sein sollte. Dieser Teil ist im obigen Code hervorgehoben. Dadurch erhalten wir einen zusätzlichen Vorteil: Wir können jeden EA zum Üben und Trainieren auf dem Wiedergabedienst verwenden. Bevor jemand eine mögliche Frage stellt, werde ich sie beantworten: Das Market Replay IST KEIN Strategietester. Es ist für diejenigen gedacht, die das Lesen des Marktes üben und so Stabilität erreichen wollen, indem sie ihre Wahrnehmung von Vermögensbewegungen verbessern. Die Marktwiedergabe ersetzt nicht den großartigen MetaTrader 5 Strategietester. Der Strategietester ist jedoch nicht zum Üben der Marktwiedergabe geeignet.

Obwohl die Umstellung auf den ersten Blick keine Nebenwirkungen zu haben scheint, ist dies nicht ganz richtig. Wenn Sie das Wiedergabesystem so ausführen, dass die Steuerung durch den Indikator anstelle des Expert Advisors erfolgt, werden Sie einen Fehler feststellen. Wenn der Zeitrahmen des Charts geändert wird, wird der Indikator aus dem Chart entfernt und dann neu gestartet. Durch dieses Entfernen und Wiedereinschalten stimmt die Schaltfläche, die anzeigt, ob wir uns im Pausen- oder im Wiedergabemodus befinden, nicht mit dem tatsächlichen Status des Wiedergabesystems überein. Um dies zu beheben, müssen wir eine kleine Anpassung vornehmen. Wir werden also den folgenden Indikator-Startcode haben:

int OnInit()
{
        u_Interprocess Info;
        
        IndicatorSetString(INDICATOR_SHORTNAME, "Market Replay");
        if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.Value = 0;
        Control.Init(Info.s_Infos.isPlay);
        
        return INIT_SUCCEEDED;
}


Die hervorgehobenen Ergänzungen im Code stellen sicher, dass der Status des Wiedergabedienstes mit der Schaltfläche übereinstimmt, die wir im Chart sehen. Änderungen im Steuercode sind recht einfach und erfordern keine besondere Aufmerksamkeit.

Nun wird die Vorlagendatei, die zuvor vom EA verwendet wurde, auf den Indikator umgestellt. Damit sind wir völlig frei, um in Zukunft weitere Änderungen vorzunehmen.


Implementierung der Positionskontrolle

Hier werden wir ein Steuerelement implementieren, das angibt, wohin wir innerhalb der Wiedergabedatei gehen wollen, um unsere Marktstudie zu starten. Aber das wird keine präzise Angabe sein. Die Ausgangsposition ist nur ungefähr. Und das liegt nicht daran, dass es unmöglich wäre, so etwas zu tun. Im Gegenteil, es wäre viel einfacher, eine präzise Angabe zumachen. In Gesprächen und beim Erfahrungsaustausch mit denjenigen, die mehr Erfahrung auf dem Markt haben, haben wir jedoch einen Konsens gefunden. Ideal ist es, nicht zu einem exakten Punkt zu gehen, an dem wir bereits eine bestimmte Bewegung erwarten, sondern die Wiedergabe an einem Punkt in der Nähe der gewünschten Bewegung zu beginnen. Mit anderen Worten: Sie müssen erst verstehen, was vor sich geht, bevor Sie Maßnahmen ergreifen können.

Diese Idee erschien mir so gut, dass ich beschloss: Die Marktwiedergabe sollte nicht zu einem bestimmten Punkt springen. Auch wenn es einfacher zu realisieren wäre, müssen Sie den nächstgelegenen Punkt ansteuern. Welcher Punkt der nächstgelegene ist, hängt von der Anzahl der pro Tag getätigten Geschäfte ab. Je mehr Geschäfte wir abschließen, desto schwieriger wird es, den richtigen Punkt zu treffen.

Wir werden also auf einen nahe gelegenen Punkt zugreifen, um zu verstehen, was tatsächlich passiert, um eine Handelssimulation zu erstellen. Nochmals: Wir erstellen KEINEN Strategietester. Auf diese Weise kann man mit der Zeit zu erkennen, wann eine Bewegung sicherer ist oder wann das Risiko zu hoch ist und Sie den Handel nicht eingehen sollten.

Alle Arbeiten in diesem Schritt werden innerhalb der Klasse C_Control durchgeführt. So, und jetzt an die Arbeit!

Als Erstes werden wir einige Definitionen festlegen.

#define def_ButtonLeft  "Images\\Market Replay\\Left.bmp"
#define def_ButtonRight "Images\\Market Replay\\Right.bmp"
#define def_ButtonPin   "Images\\Market Replay\\Pin.bmp"


Nun müssen wir eine Reihe von Variablen erstellen, um die Daten des Positionssystems zu speichern. Sie werden wie folgt umgesetzt:

struct st_00
{
        string  szBtnLeft,
                szBtnRight,
                szBtnPin,
                szBarSlider;
        int     posPinSlider,
                posY;
}m_Slider;


Das ist genau das, was Sie gerade bemerkt haben. Wir werden einen Schieberegler verwenden, um eine ungefähre Position zu bestimmen, an der das Wiedergabesystem beginnen soll. Wir haben nun eine generische Funktion, mit der die Schaltflächen für Wiedergabe/Pause und Schieberegler erstellt werden können. Diese Funktion ist im Folgenden dargestellt. Ich glaube nicht, dass es Schwierigkeiten geben wird, es zu verstehen, da es recht einfach ist.

inline void CreateObjectBitMap(int x, int y, string szName, string Resource1, string Resource2 = NULL)
                        {
                                ObjectCreate(m_id, szName, OBJ_BITMAP_LABEL, 0, 0, 0);
                                ObjectSetInteger(m_id, szName, OBJPROP_XDISTANCE, x);
                                ObjectSetInteger(m_id, szName, OBJPROP_YDISTANCE, y);
                                ObjectSetString(m_id, szName, OBJPROP_BMPFILE, 0, "::" + Resource1);
                                ObjectSetString(m_id, szName, OBJPROP_BMPFILE, 1, "::" + (Resource2 == NULL ? Resource1 : Resource2));
                        }


Nun, jetzt wird jede Schaltfläche mit dieser Funktion erstellt. Dies erleichtert die Arbeit und erhöht die Wiederverwendung des Codes, was zu mehr Stabilität und Geschwindigkeit führt. Als Nächstes muss eine Funktion erstellt werden, die den im Schieberegler zu verwendenden Kanal darstellt. Sie wird durch die folgende Funktion erstellt:

inline void CreteBarSlider(int x, int size)
                        {
                                ObjectCreate(m_id, m_Slider.szBarSlider, OBJ_RECTANGLE_LABEL, 0, 0, 0);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_XDISTANCE, x);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_YDISTANCE, m_Slider.posY - 4);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_XSIZE, size);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_YSIZE, 9);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BGCOLOR, clrLightSkyBlue);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BORDER_COLOR, clrBlack);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_WIDTH, 3);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BORDER_TYPE, BORDER_FLAT);
                        }


Interessant ist hier vor allem die Darstellung der Grenzen des Kontrollkanals. Diese können Sie nach Belieben anpassen, ebenso wie die Breite des Kanals, die in der Eigenschaft OBJPROP_YSIZE festgelegt wird. Aber wenn Sie den Wert dieser Eigenschaft ändern, vergessen Sie nicht, den Wert, der m_Slider.posY subtrahiert, so anzupassen, dass der Kanal zwischen den Schaltflächen liegt.

Die Funktion, die die Wiedergabe-/Pause-Schaltflächen erstellt, sieht nun wie folgt aus:

void CreateBtnPlayPause(long id, bool state)
{
        m_szBtnPlay = def_PrefixObjectName + "Play";
        CreateObjectBitMap(5, 25, m_szBtnPlay, def_ButtonPause, def_ButtonPlay);
        ObjectSetInteger(id, m_szBtnPlay, OBJPROP_STATE, state);
}


Das ist doch viel einfacher, oder? Schauen wir uns nun die Funktion an, mit der die Schieberegler erstellt werden. Sie ist unten aufgeführt:

void CreteCtrlSlider(void)
{
        u_Interprocess Info;
                                
        m_Slider.szBarSlider = def_PrefixObjectName + "Slider Bar";
        m_Slider.szBtnLeft   = def_PrefixObjectName + "Slider BtnL";
        m_Slider.szBtnRight  = def_PrefixObjectName + "Slider BtnR";
        m_Slider.szBtnPin    = def_PrefixObjectName + "Slider BtnP";
        m_Slider.posY = 40;
        CreteBarSlider(82, 436);
        CreateObjectBitMap(52, 25, m_Slider.szBtnLeft, def_ButtonLeft);
        CreateObjectBitMap(516, 25, m_Slider.szBtnRight, def_ButtonRight);
        CreateObjectBitMap(def_MinPosXPin, m_Slider.posY, m_Slider.szBtnPin, def_ButtonPin);
        ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_ANCHOR, ANCHOR_CENTER);
        if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.Value = 0;
        PositionPinSlider(Info.s_Infos.iPosShift);
}


Schauen Sie sich die Bezeichnungen der Bedienelemente genau an, das ist sehr wichtig. Diese Steuerelemente sind nicht verfügbar, wenn sich das Wiedergabesystem im Wiedergabestatus befindet. Die Funktion wird jedes Mal aufgerufen, wenn wir uns im Pausenzustand befinden und die Schieberegler erstellen. Beachten Sie, dass wir deshalb den Wert der globalen Variable des Terminals erfassen, um den Schieberegler korrekt zu identifizieren und zu positionieren.

Daher empfehle ich Ihnen, die globale Variable des Terminals nicht manuell zu verändern. Achten Sie bitte auf ein weiteres wichtiges Detail, den Stift. Anders als bei Knöpfen befindet sich der Ankerpunkt genau in der Mitte, sodass er leicht zu finden ist. Hier haben wir einen weiteren Funktionsaufruf:

inline void PositionPinSlider(int p)
{
        m_Slider.posPinSlider = (p < 0 ? 0 : (p > def_MaxPosSlider ? def_MaxPosSlider : p));
        ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_XDISTANCE, m_Slider.posPinSlider + def_MinPosXPin);
        ChartRedraw();
}


Sie platziert den Schieberegler in einem bestimmten Bereich und stellt sicher, dass er innerhalb der oben festgelegten Grenzen bleibt.

Wie Sie sich vorstellen können, müssen wir noch kleine Anpassungen an dem System vornehmen. Jedes Mal, wenn sich der Zeitrahmen des Charts ändert, wird der Indikator zurückgesetzt, wodurch wir den aktuellen Punkt, an dem wir uns im Wiedergabesystem befinden, verlieren. Eine Möglichkeit, dies zu vermeiden, besteht darin, einige Ergänzungen zur Initialisierungsfunktion vorzunehmen. Unter Ausnutzung dieser Änderungen werden wir auch einige zusätzliche Dinge hinzufügen. Schauen wir uns an, wie die Initialisierungsfunktion jetzt aussieht:

void Init(const bool state = false)
{
        if (m_szBtnPlay != NULL) return;
        m_id = ChartID();
        ChartSetInteger(m_id, CHART_EVENT_MOUSE_MOVE, true);
        CreateBtnPlayPause(m_id, state);
        GlobalVariableTemp(def_GlobalVariableReplay);
        if (!state) CreteCtrlSlider();
        ChartRedraw();
}


Jetzt fügen wir auch den Code für die Weiterleitung von Mausbewegungsereignissen an den Indikator hinzu. Ohne diesen Zusatz gehen die Mausereignisse verloren und werden vom MetaTrader 5 nicht an den Indikator weitergegeben. Um den Schieberegler auszublenden, wenn er nicht benötigt wird, haben wir ein kleines Häkchen hinzugefügt. Wenn diese Prüfung bestätigt, dass der Schieberegler angezeigt werden soll, wird er auf dem Bildschirm angezeigt.

Nach allem, was wir bisher gesehen haben, werden Sie sich vielleicht fragen: Wie wird die Ereignisbehandlung jetzt durchgeführt? Werden wir einen superkomplexen zusätzlichen Code haben? An der Art und Weise, wie Mausereignisse behandelt werden, ändert sich nicht viel. Das Hinzufügen eines Drag-Events ist nicht sehr kompliziert. Alles, was Sie wirklich tun müssen, ist, einige Grenzen zu setzen, damit die Dinge nicht außer Kontrolle geraten. Die Umsetzung selbst ist recht einfach.

Schauen wir uns den Code der Funktion an, die all diese Ereignisse verarbeitet: DispatchMessage. Um die Erklärung zu erleichtern, sehen wir uns den Code in Teilen an. Der erste Teil ist für die Behandlung der Ereignisse von Objektklicks zuständig. Sehen wir uns den folgenden Code an:

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        u_Interprocess Info;

//... other local variables ....
                                
        switch (id)
        {
                case CHARTEVENT_OBJECT_CLICK:
                        if (sparam == m_szBtnPlay)
                        {
                                Info.s_Infos.isPlay = (bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE);
                                if (!Info.s_Infos.isPlay) CreteCtrlSlider(); else
                                {
                                        ObjectsDeleteAll(m_id, def_PrefixObjectName + "Slider");
                                        m_Slider.szBtnPin = NULL;
                                }
                                Info.s_Infos.iPosShift = m_Slider.posPinSlider;
                                GlobalVariableSet(def_GlobalVariableReplay, Info.Value);
                                ChartRedraw();
                        }else   if (sparam == m_Slider.szBtnLeft) PositionPinSlider(m_Slider.posPinSlider - 1);
                        else if (sparam == m_Slider.szBtnRight) PositionPinSlider(m_Slider.posPinSlider + 1);                                                   
                break;

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

Wenn wir die Play/Pause-Taste drücken, müssen mehrere Aktionen durchgeführt werden. Eine davon besteht darin, einen Schieberegler zu erstellen, wenn wir uns in einem Pausenzustand befinden. Wenn wir den Pausenzustand verlassen und in den Abspielzustand wechseln, müssen die Steuerelemente aus dem Chart ausgeblendet werden, damit wir nicht mehr auf sie zugreifen können. Der aktuelle Wert des Schiebereglers sollte an die globale Variable des Terminals gesendet werden. So hat der Wiedergabedienst Zugriff auf die prozentuale Position, in der wir das Wiedergabesystem platzieren oder platzieren wollen.

Zusätzlich zu diesen Problemen im Zusammenhang mit den Wiedergabe-/Pause-Schaltflächen müssen wir uns auch mit den Ereignissen befassen, die auftreten, wenn wir auf die Schaltflächen für die punktweise Verschiebung der Bildlaufleiste klicken. Wenn wir auf die linke Schaltfläche der Bildlaufleiste klicken, sollte der aktuelle Wert des Schiebereglers um eins sinken. Ähnlich verhält es sich, wenn wir die rechte Taste der Bildlaufleiste drücken, dann sollte sie dem Steuerelement bis zur eingestellten Höchstgrenze einen Eintrag hinzufügen.

Das ist ganz einfach. Zumindest in diesem Teil ist es nicht so schwer, mit Meldungen der Objektklicks umzugehen. Allerdings gibt es jetzt ein etwas komplexeres Problem beim Ziehen des Schiebereglers. Um dies zu verstehen, sehen wir uns den Code an, der Mausbewegungsereignisse verarbeitet. Sie ist unten aufgeführt:

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        u_Interprocess Info;
        static int six = -1, sps;
        int x, y, px1, px2;
                                
        switch (id)
        {

// ... Object click EVENT ...

                case CHARTEVENT_MOUSE_MOVE:
                        x = (int)lparam;
                        y = (int)dparam;
                        px1 = m_Slider.posPinSlider + def_MinPosXPin - 14;
                        px2 = m_Slider.posPinSlider + def_MinPosXPin + 14;
                        if ((((uint)sparam & 0x01) == 1) && (m_Slider.szBtnPin != NULL))
                        {
                                if ((y >= (m_Slider.posY - 14)) && (y <= (m_Slider.posY + 14)) && (x >= px1) && (x <= px2) && (six == -1))
                                {
                                        six = x;
                                        sps = m_Slider.posPinSlider;
                                        ChartSetInteger(m_id, CHART_MOUSE_SCROLL, false);
                                }
                                if (six > 0) PositionPinSlider(sps + x - six);
                        }else if (six > 0)
                        {
                                six = -1;
                                ChartSetInteger(m_id, CHART_MOUSE_SCROLL, true);
                        }
                        break;
        }
}


Es scheint etwas komplexer zu sein, aber eigentlich ist es so einfach wie die Handhabung von Objektklicks. Der einzige Unterschied besteht darin, dass wir jetzt mehr Variablen verwenden müssen, und einige von ihnen müssen statisch sein, damit der Wert zwischen Aufrufen nicht verloren geht. Wenn die Maus bewegt wird, sendet MetaTrader 5 eine Nachricht an unser System. Wir sollten diese Meldung verwenden, um herauszufinden, was passiert ist, und um herauszufinden, wo sich der Mauszeiger befindet, welche Tasten gedrückt wurden, oder um andere Informationen zu erhalten. Alle diese Informationen sind in der Nachricht enthalten, die der MetaTrader 5 an unsere Anwendung sendet.

Wenn die linke Taste gedrückt wird, gibt es etwas zu tun. Aber um sicherzustellen, dass der Schieberegler auf dem Bildschirm erscheint und wir kein falsches Positiv erhalten, bieten wir einen zusätzlichen Test an, um die Integrität unserer Arbeit zu gewährleisten.

Wenn der Test anzeigt, dass das Ereignis gültig ist, führen wir einen weiteren Test durch, um zu prüfen, ob wir auf den Schieberegler klicken, d. h. in den Bereich, der zum Schieberegler gehört. Gleichzeitig überprüfen wir, ob diese Position noch gültig ist, denn es kann vorkommen, dass der Klick bereits erfolgt ist, die Position aber nicht mehr gültig ist. In diesem Fall sollten wir sie ignorieren. Wenn diese Prüfung erfolgreich ist, werden sowohl die Klickposition als auch der Wert des Steuerelements gespeichert. Wir müssen auch das Ziehen des Charts sperren. Dies ist für den nächsten Schritt erforderlich, in dem wir die Position des Schiebereglers auf der Grundlage der vorherigen Werte im Steuerelement berechnen. Es ist sehr wichtig, diese Daten vor jeder Berechnung zu speichern, da dies die Einrichtung und das Verständnis für die Vorgehensweise in diesem Fall erleichtert. Aber so wie es hier gemacht wird, ist es sehr einfach zu implementieren, da die Berechnung tatsächlich die Berechnung der Abweichung sein wird. 

Wenn die linke Taste losgelassen wird, kehrt die Situation in den ursprünglichen Modus zurück. Das heißt, das Chart kann wieder gezogen werden, und die statische Variable, die zum Speichern der Mausposition verwendet wird, hat einen Wert, der anzeigt, dass keine Position analysiert wird. Die gleiche Methode kann auch zum Ziehen und Ablegen von Objekten auf dem Chart verwendet werden, was ein weiterer großer Vorteil ist. All dies geschieht durch Klicken und Ziehen. Dann müssen Sie nur noch analysieren, wo sich die Region befindet, die Klicks erhalten kann. Optimieren Sie dies, und der Rest wird vom Code erledigt. Er wird wie der oben gezeigte Code aussehen.

Nachdem wir dies getan haben, haben wir bereits ein gewünschtes Verhalten in den Steuerelementen. Aber wir sind noch nicht fertig. Wir müssen den Dienst zwingen, den Wert zu verwenden, den wir im Schieberegler angeben. Wir werden dies im nächsten Thema umsetzen.


Anpassen der Klasse C_Replay

Die Dinge sind nie genau so, wie sich manche Leute das vorstellen. Nur weil wir einen Schieberegler erstellt und etwas in der Steuerklasse (C_Control) eingerichtet haben, heißt das nicht, dass alles perfekt funktioniert. Wir müssen einige Anpassungen an der Klasse vornehmen, die das Replay tatsächlich erstellt.

Diese Anpassungen sind nicht sehr kompliziert. In der Tat gibt es nur sehr wenige von ihnen, und sie befinden sich in ganz bestimmten Bereichen. Es ist jedoch zu beachten, dass sich Änderungen an einer Klasse auch auf die andere auswirken. In anderen Punkten müssen Sie aber nicht unbedingt Änderungen vornehmen. Ich ziehe es vor, keine unnötigen Punkte zu berühren und die Kapselung so weit wie möglich voranzutreiben, um so die Komplexität des gesamten Systems zu verbergen.

Kommen wir gleich zum Kern der Sache. Als erstes muss die Funktion Event_OnTime eingerichtet werden. Sie ist für das Hinzufügen gehandelter Ticks zum Replikations-Asset verantwortlich. Im Grunde genommen werden wir diese Funktion um eine Kleinigkeit erweitern. Sehen Sie sich den nachstehenden Code an:

#define macroGetMin(A)  (int)((A - (A - ((A % 3600) - (A % 60)))) / 60)
inline int Event_OnTime(void)
{
        bool isNew;
        int mili, test;
        static datetime _dt = 0;
        u_Interprocess Info;
                                
        if (m_ReplayCount >= m_ArrayCount) return -1;
        if (m_dt == 0)
        {
                m_Rate[0].close = m_Rate[0].open =  m_Rate[0].high = m_Rate[0].low = m_ArrayInfoTicks[m_ReplayCount].Last;
                m_Rate[0].tick_volume = 0;
                m_Rate[0].time = m_ArrayInfoTicks[m_ReplayCount].dt - 60;
                CustomRatesUpdate(def_SymbolReplay, m_Rate, 1);
                _dt = TimeLocal();
        }
        isNew = m_dt != m_ArrayInfoTicks[m_ReplayCount].dt;
        m_dt = (isNew ? m_ArrayInfoTicks[m_ReplayCount].dt : m_dt);
        mili = m_ArrayInfoTicks[m_ReplayCount].milisec;
        do
        {
                while (mili == m_ArrayInfoTicks[m_ReplayCount].milisec)
                {
                        m_Rate[0].close = m_ArrayInfoTicks[m_ReplayCount].Last;
                        m_Rate[0].open = (isNew ? m_Rate[0].close : m_Rate[0].open);
                        m_Rate[0].high = (isNew || (m_Rate[0].close > m_Rate[0].high) ? m_Rate[0].close : m_Rate[0].high);
                        m_Rate[0].low = (isNew || (m_Rate[0].close < m_Rate[0].low) ? m_Rate[0].close : m_Rate[0].low);
                        m_Rate[0].tick_volume = (isNew ? m_ArrayInfoTicks[m_ReplayCount].Vol : m_Rate[0].tick_volume + m_ArrayInfoTicks[m_ReplayCount].Vol);
                        isNew = false;
                        m_ReplayCount++;
                }
                mili++;
        }while (mili == m_ArrayInfoTicks[m_ReplayCount].milisec);
        m_Rate[0].time = m_dt;
        CustomRatesUpdate(def_SymbolReplay, m_Rate, 1);
        mili = (m_ArrayInfoTicks[m_ReplayCount].milisec < mili ? m_ArrayInfoTicks[m_ReplayCount].milisec + (1000 - mili) : m_ArrayInfoTicks[m_ReplayCount].milisec - mili);
        test = (int)((m_ReplayCount * def_MaxPosSlider) / m_ArrayCount);
        GlobalVariableGet(def_GlobalVariableReplay, Info.Value);
        if (Info.s_Infos.iPosShift != test)
        {
                Info.s_Infos.iPosShift = test;
                GlobalVariableSet(def_GlobalVariableReplay, Info.Value);
        }
                        
        return (mili < 0 ? 0 : mili);
};
#undef macroGetMin


In dieser Funktion erstellen wir 1-Minuten-Balken. Achten Sie darauf, dass wir in diesem Teil eine Variable hinzugefügt haben, die im obigen Code nicht vorhanden war. Wir haben nun eine Darstellung der relativen prozentualen Position, die in der globalen Variable des Terminals gespeichert ist. Daher benötigen wir diese Variable, um den in der Terminal-Variable gespeicherten internen Inhalt zu dekodieren. Sobald der gehandelte Tick zum 1-Minuten-Balken hinzugefügt wurde, müssen wir wissen, wie hoch der Prozentsatz der aktuellen Wiederholungsposition ist. Dies geschieht in dieser Berechnung, in der wir die relative Position im Verhältnis zur Gesamtzahl der gespeicherten Ticks ermitteln.

Dieser Wert wird dann mit dem Wert verglichen, der in der globalen Variable des Terminals gespeichert ist. Wenn sie unterschiedlich sind, aktualisieren wir den Wert, damit das System beim Anhalten die richtige relative Position anzeigt. Auf diese Weise müssen wir keine zusätzlichen Berechnungen anstellen und stoßen nicht auf unnötige Probleme.

Damit ist die erste Phase abgeschlossen. Wir haben jedoch noch ein weiteres Problem zu lösen. Wie lässt sich das Wiedergabesystem in die gewünschte relative Position bringen, nachdem der Wert während der Pause angepasst wurde?

Dieses Problem ist ein wenig komplizierter. Das liegt daran, dass wir sowohl Addition, die einfacher zu lösen ist, als auch Subtraktion, die etwas komplizierter ist, haben können. Diese Subtraktion ist nicht das große Problem, zumindest nicht in diesem Stadium der Entwicklung. Aber in der nächsten Phase, die wir im nächsten Artikel dieser Reihe behandeln werden, wird es ein solches Problem sein. Als Erstes muss jedoch eine zusätzliche Funktion in die Klasse C_Replay aufgenommen werden, um Takte zum Wiedergabesystem hinzuzufügen oder von ihm zu entfernen. Schauen wir uns die Vorbereitung dieser Funktion an:

int AdjustPositionReplay()
{
        u_Interprocess Info;
        int test = (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_ArrayCount);
                                
        Info.Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (Info.s_Infos.iPosShift == test) return 0;
        test = (int)(m_ArrayCount * ((Info.s_Infos.iPosShift * 1.0) / def_MaxPosSlider));
        if (test < m_ReplayCount)
        {
                CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX);
                CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX);
                m_ReplayCount = 0;
                m_Rate[0].close = m_Rate[0].open =  m_Rate[0].high = m_Rate[0].low = m_ArrayInfoTicks[m_ReplayCount].Last;
                m_Rate[0].tick_volume = 0;
                m_Rate[0].time = m_ArrayInfoTicks[m_ReplayCount].dt - 60;
                CustomRatesUpdate(def_SymbolReplay, m_Rate, 1);
        };
        for (test = (test > 0 ? test - 1 : 0); m_ReplayCount < test; m_ReplayCount++)
                Event_OnTime();

        return Event_OnTime();
}


Im obigen Code sehen wir einen Code, der die Grundlage für dieses Anpassungssystem bildet. Wir wollen verstehen, was in diesem Basissystem geschieht. Zunächst wird der prozentuale Wert der aktuellen Position ermittelt. Vergleichen Sie dann diesen Wert mit dem Wert, der in der globalen Variable des Terminals steht. Das Steuersystem speichert diesen Wert in einer globalen Variablen. Wenn die Werte gleich sind (es handelt sich nicht um einen absoluten Wert, sondern um einen prozentualen Wert), wird die Funktion beendet, da der richtige prozentuale Wert erreicht wurde oder der Benutzer seine Position während der Systempause nicht geändert hat.

Wenn die Werte jedoch unterschiedlich sind, wird der absolute Wert auf der Grundlage des in der globalen Variablen des Terminals angegebenen Prozentwerts ermittelt. Das heißt, wir haben jetzt einen absoluten Punkt, von dem aus das Wiedergabesystem starten sollte. Es ist unwahrscheinlich, dass dieser Wert dem Zähler der Ticks entspricht, und zwar aus einer Reihe von Gründen. Ist er kleiner als der aktuelle Wert des Wiederholungszählers, werden alle in der aktuellen Ressource vorhandenen Daten gelöscht.

Das ist knifflig, aber nicht in diesem Stadium der Entwicklung. Dies wird im nächsten Schritt geschehen. Im Moment gibt es keinen Grund zur Sorge. Jetzt können wir etwas tun, was für beide Situationen gilt: neue Werte hinzufügen, bis die Position des Wiederholungszählers gleich der absoluten Position minus 1 ist. Dieses Minus 1 hat den Grund, dass diese Funktion einen Wert zurückgeben kann, der später als Verzögerung verwendet wird. Dies wird durch die Funktion Event_OnTime erreicht. 

Diese Art von Veränderung geht nie ohne Schmerzen vonstatten. Schauen wir uns an, was im System geändert werden muss. Dies wird im folgenden Code gezeigt. Es ist die einzige Stelle, die verändert wurde:

#property service
#property copyright "Daniel Jose"
#property version   "1.00"
//+------------------------------------------------------------------+
#include <Market Replay\C_Replay.mqh>
//+------------------------------------------------------------------+
input string    user01 = "WINZ21_202110220900_202110221759"; //File with ticks
//+------------------------------------------------------------------+
C_Replay        Replay;
//+------------------------------------------------------------------+
void OnStart()
{
        ulong t1;
        int delay = 3;
        long id;
        u_Interprocess Info;
        bool bTest = false;
        
        if (!Replay.CreateSymbolReplay(user01)) return;
        id = Replay.ViewReplay();
        Print("Wait for permission to start replay ...");
        while (!GlobalVariableCheck(def_GlobalVariableReplay)) Sleep(750);
        Print("Replay system started ...");
        t1 = GetTickCount64();
        while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.Value)))
        {
                if (!Info.s_Infos.isPlay)
                {
                        if (!bTest) bTest = (Replay.Event_OnTime() > 0);
                }else
                {
                        if (bTest)
                        {
                                delay = ((delay = Replay.AdjustPositionReplay()) == 0 ? 3 : delay);
                                bTest = false;
                                t1 = GetTickCount64();
                        }else if ((GetTickCount64() - t1) >= (uint)(delay))
                        {
                                if ((delay = Replay.Event_OnTime()) < 0) break;
                                t1 = GetTickCount64();
                        }
                }
        }
        Replay.CloseReplay();
        Print("Replay system stopped ...");
}
//+------------------------------------------------------------------+


Während wir uns im Pausenmodus befinden, führen wir diesen Test durch, um zu sehen, ob wir den Status des Dienstes ändern. Wenn dies geschieht, wird die Klasse C_Replay aufgefordert, die neue Positionierung vorzunehmen, was auch ausgeführt werden könnte oder nicht.

Wenn das ausgeführt wird, erhalten wir den Wert der nächsten Verzögerung, der nach dieser Anpassung und der Positionierung des Systems verwendet wird. Falls erforderlich, wird die verbleibende Zeit natürlich fortgesetzt, bis wir den Wiedergabestatus verlassen und in den Pausenzustand übergehen. Dann wird der gesamte Vorgang noch einmal wiederholt.


Schlussfolgerung

Das Video zeigt das gesamte System in Betrieb, Sie können sehen, wie alles abläuft. Es ist jedoch wichtig zu beachten, dass Sie warten müssen, bis sich die Lage stabilisiert hat, bevor Sie das Wiedergabesystem nutzen können. Wenn Sie eine Position zum gewünschten Punkt verschieben, kann die Bewegung schwierig erscheinen.

Diese Situation wird in Zukunft korrigiert werden. Aber für den Moment können wir das verkraften, denn wir haben noch eine Menge zu klären.



Im Anhang habe ich zwei reale Markt-Tick-Dateien beigefügt, damit Sie mit dem Bewegungs- und Positionierungssystem an Tagen mit einer unterschiedlichen Anzahl von gehandelten Ticks experimentieren können. Sie können also sehen, wie das Prozentsystem funktioniert. Dies erschwert die Sache für diejenigen, die einen bestimmten Zeitpunkt auf dem Markt studieren wollen, aber genau das ist unsere Absicht, wie zu Beginn des Artikels erläutert.

Mit diesem Replay-System, das wir hier aufbauen, werden Sie wirklich lernen, den Markt zu analysieren. Es wird keinen genauen Ort geben, an dem Sie sagen: „HIER... hier sollte ich eintreten.“ Es kann nämlich passieren, dass die Bewegung, die Sie beobachtet haben, ein paar Takte weiter stattfindet. Deshalb müssen Sie lernen, wie man den Markt analysiert, sonst wird Ihnen das in dieser Artikelserie vorgestellte Replay-System nicht gefallen.


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

Beigefügte Dateien |
Market_Replay.zip (10795.89 KB)
Verbessern Sie Ihre Handelscharts durch interaktiven GUI's in MQL5 (Teil II): Ein bewegliches GUI (II) Verbessern Sie Ihre Handelscharts durch interaktiven GUI's in MQL5 (Teil II): Ein bewegliches GUI (II)
Erschließen Sie das Potenzial der dynamischen Datendarstellung in Ihren Handelsstrategien und Dienstprogrammen mit unserer ausführlichen Anleitung zur Erstellung beweglicher GUIs in MQL5. Tauchen Sie ein in die grundlegenden Prinzipien der objektorientierten Programmierung und entdecken Sie, wie Sie mit Leichtigkeit und Effizienz einzelne oder mehrere bewegliche GUIs auf demselben Diagramm entwerfen und implementieren können.
Mean Reversion, eine einfache Handelsstrategie Mean Reversion, eine einfache Handelsstrategie
Mean Reversion ist eine Form des entgegengesetzten Handels, bei der der Händler erwartet, dass der Kurs zu einer Art Gleichgewicht zurückkehrt, das im Allgemeinen durch einen Mittelwert oder eine andere Statistik der zentralen Tendenz gemessen wird.
Entwicklung eines MQTT-Clients für MetaTrader 5: ein TDD-Ansatz Entwicklung eines MQTT-Clients für MetaTrader 5: ein TDD-Ansatz
Dieser Artikel berichtet über die ersten Versuche bei der Entwicklung eines nativen MQTT-Clients für MQL5. MQTT ist ein Client-Server-Publish/Subscribe-Messaging-Transportprotokoll. Es ist leichtgewichtig, offen, einfach und so konzipiert, dass sie leicht zu implementieren ist. Diese Eigenschaften machen es ideal für den Einsatz in vielen Situationen.
Das Erstellen von grafischen Panels ist mit MQL5 einfach geworden Das Erstellen von grafischen Panels ist mit MQL5 einfach geworden
In diesem Artikel bieten wir eine einfache und leicht verständliche Anleitung für jeden, der eines der wertvollsten und hilfreichsten Werkzeuge im Handel erstellen muss, nämlich das grafische Panel zur Vereinfachung und Erleichterung von Aufgaben rund um den Handel, das dabei hilft, Zeit zu sparen und sich ohne Ablenkungen mehr auf den eigentlichen Handelsprozess zu konzentrieren.