Einen handelnden Expert Advisor von Grund auf neu entwickeln (Teil 8): Ein konzeptioneller Sprung

Daniel Jose | 22 Juni, 2022

Einführung

Manchmal finden wir bei der Entwicklung von Projekten neue ideale und mögliche neue Funktionen, die sich als nützlich erweisen und das System, das wir entwickeln, erheblich verbessern können. Es stellt sich jedoch die Frage, wie sich die neue Funktion am einfachsten implementieren lässt.

Das Problem ist jedoch, dass wir manchmal alles, was bereits entwickelt wurde, vergessen und von vorne anfangen müssen. Das ist ziemlich demotivierend. Mit der Zeit, nach über 20 Jahren C++-Programmierung, habe ich eine bestimmte Denkweise entwickelt. Wir entwickeln einige Konzepte, die uns helfen, Dinge zu planen und Änderungen mit minimalem Aufwand vorzunehmen, aber manchmal können sich die Dinge ändern und viel komplexer werden, als wir ursprünglich angenommen haben.

Bis jetzt haben wir den EA so aufgebaut, dass er neuen Code aufnehmen kann, ohne die aktuelle Funktionalität zu verlieren: Wir haben einfach Klassen erstellt und hinzugefügt. Jetzt müssen wir einen Schritt zurückgehen und dann zwei Schritte vorwärts machen. Dieser Schritt zurück wird es uns ermöglichen, neue Funktionen einzuführen. Bei dieser Funktionalität handelt es sich um eine Fensterklasse mit einigen Informationen auf der Grundlage von Vorlagen; dies wird hier der erste Teil sein. Wir werden den Code radikal ändern, dabei aber alle Funktionen beibehalten, die wir im Moment haben, und im zweiten Teil werden wir uns mit der IDE beschäftigen.


Planung

Unser Expert Advisor ist derzeit in einer Objektklasse strukturiert. Dies geht aus dem nachstehenden Chart hervor.

Das System funktioniert derzeit gut und ist sehr stabil. Aber jetzt müssen wir den EA wie unten gezeigt umstrukturieren. Sie werden feststellen, dass es eine zusätzliche Klasse gibt, während sich die Positionen von C_TemplateChart und C_SubWindow geändert haben.


Was ist der Zweck einer solchen Umstrukturierung? Das Problem besteht darin, dass die Art und Weise, in der schwebende Fenster implementiert wurden, nicht für Fenster geeignet ist, die Daten des Assets enthalten, und daher solche Änderungen erforderlich sind. Diese Änderung ist jedoch nicht nur ästhetisch in Bezug auf die Struktur, sondern erforderte auch eine extreme Änderung des Codes, sodass er sich sehr von dem vorherigen Code unterscheiden wird.

Machen wir uns also an die Arbeit.


Umsetzung in der Praxis

1. Änderungen im internen Code des Expert Advisors

Die erste große Änderung beginnt in der EA-Initialisierungsdatei. Siehe den nachstehenden Code:

input group "Window Indicators"
input string                    user01 = "";                    //Subwindow indicators
input group "WallPaper"
input string                    user10 = "Wallpaper_01";        //Used BitMap
input char                      user11 = 60;                    //Transparency (from 0 to 100)
input C_WallPaper::eTypeImage   user12 = C_WallPaper::IMAGEM;   //Chart background type
input group "Chart Trader"
input int                       user20   = 1;                   //Leverage factor
input int                       user21   = 100;                 //Take Profit (financial)
input int                       user22   = 75;                  //Stop Loss (financial)
input color                     user23   = clrBlue;             //Price line color
input color                     user24   = clrForestGreen;      //Take Profit line color
input color                     user25   = clrFireBrick;        //Stop line color
input bool                      user26   = true;                //Day Trade?
input group "Volume At Price"
input color                     user30  = clrBlack;             //Bar color
input char                      user31  = 20;                   //Transparency (from 0 to 100 )
//+------------------------------------------------------------------+
C_TemplateChart Chart;
C_WallPaper     WallPaper;
C_VolumeAtPrice VolumeAtPrice;
//+------------------------------------------------------------------+
int OnInit()
{
        static string   memSzUser01 = "";
        
        Terminal.Init();
        WallPaper.Init(user10, user12, user11);
        if (memSzUser01 != user01)
        {
                Chart.ClearTemplateChart();
                Chart.AddThese(memSzUser01 = user01);
        }
        Chart.InitilizeChartTrade(user20, user21, user22, user23, user24, user25, user26);
        VolumeAtPrice.Init(user24, user25, user30, user31);
        
   OnTrade();
        EventSetTimer(1);
   
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+


Bedenken wir, dass wir jetzt nur noch eine Variable haben, die angibt, welche Vorlagen geladen werden sollen. Der Rest des Codes scheint bis auf den hervorgehobenen Teil unverändert zu sein. Es ist vielleicht nicht ganz klar, was er hier tut, oder warum dieser hervorgehobene Code im EA-Initialisierungsteil platziert wurde. Wenn wir den EA auf ein Chart laden, werden einige Dinge erstellt, die bei normalem Gebrauch geändert werden können. Zuvor war es sinnlos, den hervorgehobenen Code hinzuzufügen, da alles zusammen funktionieren sollte und sich Änderungen nicht auf das Verhalten oder das Aussehen von EA auswirkten. Aber wenn wir fließende Indikatoren hinzufügen, passiert etwas Ärgerliches: Jedes Mal, wenn wir den Zeitrahmen ändern, startet der EA neu und die Fenster kehren in ihren ursprünglichen Zustand zurück. Wenn wir sie nicht zerstören, sammeln sich nutzlose Dinge auf dem Chart an, und wenn wir sie nicht richtig zerstören, werden sie an ihrem ursprünglichen Platz wieder aufgebaut, was ebenfalls eine große Unannehmlichkeit darstellt. Wenn der Nutzer die erforderlichen Vorlagen nicht ändert, verhindert der hervorgehobene Code, dass schwebende Fenster fälschlicherweise zerstört werden, aber wenn Änderungen an den Vorlagen vorgenommen werden, wird der EA normal neu gestartet. Dies ist sehr einfach und dennoch äußerst effizient.

Der nächste Punkt, den es zu beachten gilt, betrifft das interne Nachrichtensystem. Zuvor gab es eine zusätzliche Variable, die wir aber entfernt haben, sodass der Code jetzt wie folgt aussieht:

void OnTick()
{
        Chart.DispatchMessage(CHARTEVENT_CHART_CHANGE, 0, NanoEA.CheckPosition(), C_Chart_IDE::szMsgIDE[C_Chart_IDE::eRESULT]);
}


Das System nutzt nun das MQL5-Nachrichtenaustauschsystem effizienter, während das Senden von Nachrichten der Funktion selbst sehr ähnlich ist. Dies ermöglicht die Übergabe von Parametern, ohne die Objektklasse zu belasten, sodass jede Klasse die vom MetaTrader 5-System erzeugten Ereignismeldungen auf die am besten geeignete Weise verarbeiten kann. Auf diese Weise wird jede Objektklasse weiter isoliert, und der EA kann für jeden Nutzertyp und mit weniger Aufwand vielfältigere Formen annehmen.


2. Änderungen im Code zur Unterstützung von Unterfenstern

Bis zu diesem Punkt war der Code des Unterfensters sehr einfach, aber es gab ein Problem: Aus irgendeinem Grund konnte der EA das erstellte Unterfenster nicht löschen. Beim erneuten Öffnen des Expert Advisors wurde ein neues Unterfenster erstellt, wodurch die Kontrolle über das System verloren ging. Überraschenderweise war es sehr einfach, das Problem zu beheben. Werfen wir zunächst einen Blick auf das unten abgebildete Fragment der Unterstützungsdatei:

int OnInit()
{
        IndicatorSetString(INDICATOR_SHORTNAME, "SubWinSupport");
        
        return INIT_SUCCEEDED;
}

Mit der hervorgehobenen Zeile wird ein Alias für die Unterstützungsdatei erstellt. Der EA sieht diesen Alias EA und prüft, ob das Teilfenstersystem geladen ist oder nicht. Dies geschieht unabhängig vom Dateinamen, da der EA nur den Alias überprüft. Derselbe Codetyp wird später zur Unterstützung anderer Dinge im EA verwendet werden. Ich werde hier nicht zu sehr ins Detail gehen, aber später, in einem anderen Artikel, werde ich erklären, wie man die Vorteile des hervorgehobenen Codes nutzen kann.

Nachdem dies nun geschehen ist, wollen wir uns den Code zum Laden und Erstellen von Unterfenstern ansehen. Sie ist unten dargestellt:

void Init(void)
{
        int i0;
        if ((i0 = ChartWindowFind(Terminal.Get_ID(), def_Indicador)) == -1)
                ChartIndicatorAdd(Terminal.Get_ID(), i0 = (int)ChartGetInteger(Terminal.Get_ID(), CHART_WINDOWS_TOTAL), iCustom(NULL, 0, "::" + def_Resource));
        m_IdSubWinEA = i0;
}


Wie Sie sehen können, ist es viel einfacher, aber dieser Code ist nicht öffentlich, sondern der Zugriff erfolgt über einen anderen Code, der öffentlich ist:

inline int GetIdSubWinEA(void)
{
        if (m_IdSubWinEA < 0) Init();
        return m_IdSubWinEA;
}


Aber warum wird sie auf diese Weise umgesetzt? Es kann vorkommen, dass der EA keine Indikatoren im Unterfenster verwendet, und wenn das System dies bemerkt, entfernt es das Unterfenster aus dem Chart und erstellt es nur bei Bedarf. Diese Entscheidung wird jedoch nicht vom EA-Code getroffen, sondern von der Klasse C_TemplateChart.


3. Neue Klasse C_TemplateChart

Schauen Sie sich die folgende Animation an:

Bedenken wir, dass wir jetzt eine vertikale Linie haben, die anzeigt, wo wir gerade analysieren. Diese Linien sind unabhängig voneinander. Sie waren vorher nicht verfügbar, was die Analyse einiger Punkte des Indikators je nach Chart erschwerte. Dies ist eine der Verbesserungen, die in der Klasse C_TemplateChart enthalten sind. Schauen wir uns den Code innerhalb der Klasse an, um die weiteren Änderungen zu verstehen, da sie erheblich waren.

Schauen wir uns den folgenden Code an, in dem die Variablen deklariert werden:

class C_TemplateChart : public C_SubWindow
{
#define def_MaxTemplates                8
#define def_NameTemplateRAD     "IDE"
//+------------------------------------------------------------------+
        private :
//+------------------------------------------------------------------+
        enum eParameter {TEMPLATE = 0, PERIOD, SCALE, WIDTH, HEIGHT};
//+------------------------------------------------------------------+
                struct st
                {
                        string          szObjName,
                                        szSymbol,
                                        szTemplate,
                                        szVLine;
                        int             width,
                                        scale;
                        ENUM_TIMEFRAMES timeframe;
                        long            handle;
                }m_Info[def_MaxTemplates];
                int     m_Counter,
                        m_CPre,
                        m_Aggregate;
                struct st00
                {
                        int     counter;
                        string  Param[HEIGHT + 1];
                }m_Params;


Das erste, was wir beachten müssen, ist, dass die Klasse C_TemplateChart die Klasse C_SubWindow erweitern wird. Dieser Teil des Codes scheint nichts Besonderes zu sein, aber achten Sie auf den hervorgehobenen Teil: Er verweist auf ein internes Datenanalysesystem, mit dem es möglich ist, vom Nutzer angeforderte Indikatoren zu erstellen und entsprechend zu präsentieren. Jetzt wurde das System zur Beschreibung der Art und Weise, wie der Nutzer die Dinge anzeigt, standardisiert. Auch wenn es verwirrend erscheint, wird es mit der Zeit klar werden. Zur Erläuterung des neuen Formats ist es notwendig, das folgende Fragment zu analysieren, das für die Analyse der Anfrage des Nutzers zuständig ist:

int GetCommand(int iArg, const string szArg)
{
        for (int c0 = TEMPLATE; c0 <= HEIGHT; c0++) m_Params.Param[c0] = "";
        m_Params.counter = 0;
        for (int c1 = iArg, c2 = 0; szArg[iArg] != 0x00; iArg++) switch (szArg[iArg])
        {
                case ')':
                case ';':
                        m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2);
                        for (; (szArg[iArg] != 0x00) && (szArg[iArg] != ';'); iArg++);
                        return iArg + 1;
                case ' ':
                        c2 += (c1 == iArg ? 0 : 1);
                        c1 = (c1 == iArg ? iArg + 1 : c1);
                        break;
                case '(':
                case ',':
                        c2 = (m_Params.counter == SCALE ? (c2 >= 1 ? 1 : c2) : c2);
                        m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2);
                        c2 = 0;
                        c1 = iArg + 1;
                        break;
                default:
                        c2++;
                        break;
        }
        return -1;
}


Wir löschen zunächst alle Daten, die sich zuvor in der Struktur befanden. Dann beginnen wir mit der Analyse und dem Empfang der einzelnen Parameter, falls es welche gibt. Sie sind nicht obligatorisch, aber sie zeigen an, wie die Dinge dem Nutzer präsentiert werden sollen. Dieses System ist selbsterweiternd. Wenn wir also weitere Informationen hinzufügen möchten, müssen Sie diese nur in der Enumeration von eParameter angeben. Derzeit verfügt das System über fünf Parameter, die in der gleichen Reihenfolge angegeben werden sollten, wie sie in der Enumeration eParameter angegeben sind. Diese Enumeration wird im Teil der Variablendeklaration hervorgehoben. Die Parameter in der richtigen Reihenfolge und ihre Erklärungen sind unten aufgeführt.

Parameter Ergebnis
1. TEMPLATE oder ASSET Gibt an, welche Vorlage oder welches Asset angezeigt werden soll
2. PERIOD Falls angegeben, wird der Indikator auf einen bestimmten festen Zeitrahmen gesetzt, so wie er zuvor verwendet wurde
3. SCALE Falls angegeben, ist der Indikator an eine feste Skala gebunden.
4. BREITE Falls angegeben, bestimmt der Parameter, wie breit der Indikator im Fenster sein wird.
5. HÖHE Dieser neue Parameter wird später in diesem Artikel behandelt - er zeigt die Verwendung eines schwebenden Fensters an.

Die einzige Struktur, die derzeit nicht von Parameter 5 profitiert, ist IDE, aber das wird behoben, und im nächsten Artikel werde ich zeigen, wie man von IDE profitieren kann. Dieser Artikel befasst sich mit anderen Systemen.

Nehmen wir nun an, dass wir dem Nutzer aus dem einen oder anderen Grund erlauben wollen, die Farbe der vertikalen Linie des Indikators zu steuern. Es ist nicht erforderlich, den Code für die Parameteranalyse zu ändern - nehmen wir die Änderung einfach wie folgt vor:

class C_TemplateChart : public C_SubWindow
{
#define def_MaxTemplates        8
#define def_NameTemplateRAD     "IDE"
//+------------------------------------------------------------------+
        private :
//+------------------------------------------------------------------+
        enum eParameter {TEMPLATE = 0, COLOR_VLINE, PERIOD, SCALE, WIDTH, HEIGHT};
//+------------------------------------------------------------------+

// ... Internal code ....

                struct st00
                {
                        int     counter;
                        string  Param[HEIGHT + 1];
                }m_Params;


Das System erkennt automatisch, dass in einem Aufruf sechs Parameter enthalten sein können. Nun ist ein weiteres Problem aufgetaucht. Der obige GetCommand-Code funktioniert zwar einwandfrei, weist aber einen Fehler auf. Oftmals bemerken wir diesen Fehler nicht, wenn wir das System, das wir verwenden wollen, selbst erstellen. Wenn das System jedoch anderen Personen zur Verfügung gestellt wird, wird der Fehler offensichtlich und kann dazu führen, dass weniger erfahrene Programmierer nicht mehr wissen, wie sie das Problem beheben können. Das ist der Grund, warum die objektorientierte Programmierung so hoch geschätzt wird - sie ermöglicht die Erstellung des am besten geeigneten Modells für die Verwendung in den Programmen von größtem Interesse. Eine der Voraussetzungen für OOP ist, dass Daten und Klassenvariablen korrekt initialisiert werden. Um dies zu gewährleisten, ist es wichtig, alles zu testen. Obwohl der GetCommand-Code korrekt zu sein scheint, enthält er einen Fehler - er überprüft nicht die maximale Parametergrenze. Wenn das ursprüngliche Modell nur fünf Parameter zulässt, was passiert dann, wenn der Nutzer sechs Parameter festlegt? Das gilt es zu vermeiden: Wir sollten nicht davon ausgehen, dass alles funktioniert, sondern garantieren, dass alles funktioniert. Daher muss der Code wie unten dargestellt korrigiert werden (die Korrekturen sind hervorgehoben).

int GetCommand(int iArg, const string szArg)
{
        for (int c0 = TEMPLATE; c0 <= HEIGHT; c0++) m_Params.Param[c0] = "";
        m_Params.counter = 0;
        for (int c1 = iArg, c2 = 0; szArg[iArg] != 0x00; iArg++) switch (szArg[iArg])
        {
                case ')':
                case ';':
                        m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2);
                        for (; (szArg[iArg] != 0x00) && (szArg[iArg] != ';'); iArg++);
                        return iArg + 1;
                case ' ':
                        c2 += (c1 == iArg ? 0 : 1);
                        c1 = (c1 == iArg ? iArg + 1 : c1);
                        break;
                case '(':
                case ',':
                        if (m_Params.counter == HEIGHT) return StringLen(szArg) + 1;
                        c2 = (m_Params.counter == SCALE ? (c2 >= 1 ? 1 : c2) : c2);
                        m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2);
                        c2 = 0;
                        c1 = iArg + 1;
                        break;
                default:
                        c2++;
                        break;
        }
        return -1;
}

Durch einfaches Hinzufügen einer einzigen Zeile verhindern wir, dass das System ein unerwartetes Ergebnis erzeugt, denn wenn der letzte erwartete Parameter HÖHE ist, aber nicht der letzte, bedeutet dies logischerweise, dass etwas falsch ist, sodass das System alles ignoriert, was danach deklariert wird, und dadurch Probleme vermeidet.

Wenn Sie nicht wissen, wie das System Muster und Parameter erkennt, finden Sie hier die Syntax:

Parameter_00 (Parameter_01, Parameter_02, Parameter_03, Parameter_04)

Wobei Parameter_00 die zu verwendende Vorlage angibt und die übrigen durch Kommas ( , ) getrennt sind und die Werte angeben, die in der Enumeration eParameter definiert sind. Wenn wir nur Parameter_03 ändern wollen, können wir den Rest leer lassen, wie in der folgenden Abbildung gezeigt. In dieser Abbildung zeige ich, dass das System so funktioniert, wie der Nutzer es wünscht.

       

Vergessen wir nicht, dass wir eine standardisierte Angabe haben, die Funktionsaufrufen sehr ähnlich ist, aber das kann verwirrend erscheinen. Folgendes ist passiert: Wir geben die RSI-Schablone an, dann geben wir weder den Zeitraum noch die Skala an. Diese Werte werden leer gelassen, damit das System versteht, dass es dem Hauptchart folgen muss. Aber wir geben die Breite und Höhe an, sodass das System versteht, dass dies in einem schwebenden Fenster angezeigt werden soll, und so erscheint der RSI-Indikator in einem schwebenden Fenster. In der ADX-Vorlage geben wir nur die Breite an, damit das System sie mit der im Teilfenster definierten Breite anzeigt. Der Stoch-Indikator wird das gesamte verbleibende Teilfenster einnehmen und sich den Platz mit dem ADX teilen. Aber wenn der Nutzer etwas ändern will, wird es nicht schwierig sein, sehen, was passiert, wenn wir die Höhe für den ADX angeben.

Das System ändert sofort die Darstellung des ADX - es platziert ihn in einem schwebenden Fenster und überlässt das gesamte Teilfenster Stoch. Jedes schwebende Fenster ist völlig unabhängig von den anderen. Aber der Tanga geht über das hinaus, was man sehen kann, siehe das nächste Bild.

Beachten wir, dass das Unterfenster gelöscht wurde, weil es nicht mehr benötigt wird. Aber welche Funktion ist für all das verantwortlich? Sie ist unten abgebildet - mit ein paar Änderungen lassen sich viele interessante Dinge erreichen:

void AddTemplate(void)
{
        ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT;
        string sz0 = m_Params.Param[PERIOD];
        int w, h, i;
        bool bIsSymbol;

        if (sz0 == "1M") timeframe = PERIOD_M1; else
        if (sz0 == "2M") timeframe = PERIOD_M2; else
        if (sz0 == "3M") timeframe = PERIOD_M3; else
        if (sz0 == "4M") timeframe = PERIOD_M4; else
        if (sz0 == "5M") timeframe = PERIOD_M5; else
        if (sz0 == "6M") timeframe = PERIOD_M6; else
        if (sz0 == "10M") timeframe = PERIOD_M10; else
        if (sz0 == "12M") timeframe = PERIOD_M12; else
        if (sz0 == "15M") timeframe = PERIOD_M15; else
        if (sz0 == "20M") timeframe = PERIOD_M20; else
        if (sz0 == "30M") timeframe = PERIOD_M30; else
        if (sz0 == "1H") timeframe = PERIOD_H1; else
        if (sz0 == "2H") timeframe = PERIOD_H2; else
        if (sz0 == "3H") timeframe = PERIOD_H3; else
        if (sz0 == "4H") timeframe = PERIOD_H4; else
        if (sz0 == "6H") timeframe = PERIOD_H6; else
        if (sz0 == "8H") timeframe = PERIOD_H8; else
        if (sz0 == "12H") timeframe = PERIOD_H12; else
        if (sz0 == "1D") timeframe = PERIOD_D1; else
        if (sz0 == "1S") timeframe = PERIOD_W1; else
        if (sz0 == "1MES") timeframe = PERIOD_MN1;
        if ((m_Counter >= def_MaxTemplates) || (m_Params.Param[TEMPLATE] == "")) return;
        bIsSymbol = SymbolSelect(m_Params.Param[TEMPLATE], true);
        w = (m_Params.Param[WIDTH] != "" ? (int)StringToInteger(m_Params.Param[WIDTH]) : 0);
        h = (m_Params.Param[HEIGHT] != "" ? (int)StringToInteger(m_Params.Param[HEIGHT]) : 0);
        i = (m_Params.Param[SCALE] != "" ? (int)StringToInteger(m_Params.Param[SCALE]) : -1);
        i = (i > 5 || i < 0 ? -1 : i);
        if ((w > 0) && (h > 0)) AddIndicator(m_Params.Param[TEMPLATE], 0, -1, w, h, timeframe, i); else
        {
                SetBase(m_Params.Param[TEMPLATE], (bIsSymbol ? m_Params.Param[TEMPLATE] : _Symbol), timeframe, i, w);
                if (!ChartApplyTemplate(m_Info[m_Counter - 1].handle, m_Params.Param[TEMPLATE] + ".tpl")) if (bIsSymbol) ChartApplyTemplate(m_Info[m_Counter - 1].handle, "Default.tpl");
                if (m_Params.Param[TEMPLATE] == def_NameTemplateRAD)
                {
                        C_Chart_IDE::Create(GetIdSubWinEA());
                        m_Info[m_Counter - 1].szVLine = "";
                }else
                {
                        m_Info[m_Counter - 1].szVLine = (string)ObjectsTotal(Terminal.Get_ID(), -1, -1) + (string)MathRand();
                        ObjectCreate(m_Info[m_Counter - 1].handle, m_Info[m_Counter - 1].szVLine, OBJ_VLINE, 0, 0, 0);
                        ObjectSetInteger(m_Info[m_Counter - 1].handle, m_Info[m_Counter - 1].szVLine, OBJPROP_COLOR, clrBlack);
                }
                ChartRedraw(m_Info[m_Counter - 1].handle);
        }
}

Die hervorgehobenen Teile zeigen die Auswahl der Art und Weise, wie die Daten auf dem Bildschirm dargestellt werden sollen. Der Code unterscheidet sich nicht wesentlich von dem bereits bestehenden. Aber diese Tests garantieren, dass sich das System so verhält, wie der Nutzer es wünscht. All dies erfordert nicht wirklich eine Umstrukturierung des Codes in einem neuen Modell, aber wenn wir uns die Funktion ansehen, die für die Größenänderung des Teilfensters verantwortlich ist, ändert sich das Bild.

void Resize(void)
{
#define macro_SetInteger(A, B) ObjectSetInteger(Terminal.Get_ID(), m_Info[c0].szObjName, A, B)
        int x0, x1, y;
        if (!ExistSubWin()) return;
        x0 = 0;
        y = (int)(ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS, GetIdSubWinEA()));
        x1 = (int)((Terminal.GetWidth() - m_Aggregate) / (m_Counter > 0 ? (m_CPre == m_Counter ? m_Counter : (m_Counter - m_CPre)) : 1));
        for (char c0 = 0; c0 < m_Counter; x0 += (m_Info[c0].width > 0 ? m_Info[c0].width : x1), c0++)
        {
                macro_SetInteger(OBJPROP_XDISTANCE, x0);
                macro_SetInteger(OBJPROP_XSIZE, (m_Info[c0].width > 0 ? m_Info[c0].width : x1));
                macro_SetInteger(OBJPROP_YSIZE, y);
                if (m_Info[c0].szTemplate == "IDE") C_Chart_IDE::Resize(x0);
        }
        ChartRedraw();
#undef macro_SetInteger
}

Die hervorgehobene Zeile verhindert, dass das System auf der alten Basis aufgebaut werden kann. Diese Zeile prüft, ob ein Unterfenster geöffnet ist und vom EA verwaltet wird, und wenn nicht, kehrt sie zurück und die Funktion tut nichts weiter. Aber wenn ein solches Unterfenster existiert, sollten alle darin befindlichen Dinge in der Größe angepasst werden, und gerade wegen dieses Tests wurde das System komplett umgestaltet.

Nachfolgend eine weitere Funktion, die geändert wurde:

void DispatchMessage(int id, long lparam, double dparam, string sparam)
{
        int mx, my;
        datetime dt;
        double p;

        C_Chart_IDE::DispatchMessage(id, lparam, dparam, sparam);
        switch (id)
        {
                case CHARTEVENT_MOUSE_MOVE:
                        mx = (int)lparam;
                        my = (int)dparam;
                        ChartXYToTimePrice(Terminal.Get_ID(), mx, my, my, dt, p);
                        for (int c0 = 0; c0 < m_Counter; c0++)  if (m_Info[c0].szVLine != "")
                        {
                                ObjectMove(m_Info[c0].handle, m_Info[c0].szVLine, 0, dt, 0);
                                ChartRedraw(m_Info[c0].handle);
                        }
                        break;
                case CHARTEVENT_CHART_CHANGE:
                        Resize();
                        for (int c0 = 0; c0 < m_Counter; c0++)
                        {
                                ObjectSetInteger(Terminal.Get_ID(), m_Info[c0].szObjName, OBJPROP_PERIOD, m_Info[c0].timeframe);
                                ObjectSetInteger(Terminal.Get_ID(), m_Info[c0].szObjName, OBJPROP_CHART_SCALE,(m_Info[c0].scale < 0 ? ChartGetInteger(Terminal.Get_ID(),CHART_SCALE):m_Info[c0].scale));
                        }
                        break;
        }
}


Der hervorgehobene Teil verdient wirklich besondere Aufmerksamkeit. Was bewirkt es? Mit diesem Code wird die vertikale Linie an der richtigen Stelle und in der richtigen Vorlage dargestellt. Der Rest des Codes dient lediglich der Pflege und Anpassung der Vorlagen, wenn sich das Chart ändert.

Es hat viele Vorteile, dies hier in der Objektklasse zu tun und nicht in der EA innerhalb des OnChartEvent-Systems. Das Wichtigste ist, dass jede Klasse das Ereignis verarbeiten kann, das MetaTrader 5 an den EA sendet. Anstatt alles in einer einzigen Funktion zu zentralisieren, lassen wir jede Klasse ihre Aufgabe erledigen, und wenn wir die Klasse im EA nicht verwenden wollen, entfernen wir sie einfach, ohne dass sich dies auf den restlichen Code auswirkt.

Programmieren ist SCHÖN, nicht wahr? Ich LIEBE Programmieren...

Bevor wir zum nächsten Punkt in diesem Artikel übergehen, möchte ich kurz auf die Werte eingehen, die in den Parametern 1 und 2 verwendet werden können. Dem Parameter 1 können die folgenden Werte zugeordnet werden: 1M, 2M, 3M, 4M, 5M, 6M, 10M, 12M, 15M, 20M, 30M, 1H, 2H, 3H, 4H, 6H, 8H, 12H, 1D, 1S, 1MONAT. Diese Werte sind nicht zufällig - sie stammen aus der Enumeration ENUM_TIMEFRAME und wiederholen genau das, was Sie tun würden, wenn Sie ein normales Chart verwenden würden. Parameter 2 kann Werte von 0 bis 5 annehmen, wobei 0 die am weitesten entfernte und 5 die am nächsten liegende Skala ist. Weitere Einzelheiten siehe .


3.4 Unterstützung für schwebende Fenster

Versuchen wir nun zu verstehen, wie schwebende Fenster erstellt und verwaltet werden, denn ohne dieses Verständnis ist es unmöglich, die Vorteile des Systems wirklich zu nutzen. Die dafür zuständige Objektklasse wurde C_ChartFloating genannt. Man könnte meinen: Warum wird nicht die Klasse aus der Standard-MQL5-Bibliothek verwendet? Der Grund dafür ist einfach. Die Steuerklasse ermöglicht es uns, ein Fenster mit einer Funktionalität zu erstellen und zu verwalten, die der des Betriebssystems auf dem Computer sehr ähnlich ist, aber für unseren Zweck zu übertrieben. Wir brauchen etwas viel Einfacheres. Die Verwendung der Steuerklasse, um das zu tun, was wir wollen, wäre wie die Verwendung einer Bazooka, um eine Fliege zu töten. Aus diesem Grund verwenden wir die Klasse C_ChartFloating - sie enthält die minimalen Elemente, die notwendig sind, um schwebende Fenster zu unterstützen, während sie uns erlaubt, sie zu steuern.

Die Klasse selbst bedarf keiner großen Erklärung, da wir lediglich 4 grafische Objekte erstellen. Unter den internen Funktionen gibt es jedoch zwei, die besondere Aufmerksamkeit verdienen. Beginnen wir mit der Funktion, die das Fenster erstellt:

bool AddIndicator(string sz0, int x = 0, int y = -1, int w = 300, int h = 200, ENUM_TIMEFRAMES TimeFrame = PERIOD_CURRENT, int Scale = -1)
{
        m_LimitX = (int)ChartGetInteger(Terminal.Get_ID(), CHART_WIDTH_IN_PIXELS);
        m_LimitY = (int)ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS);
        if (m_MaxCounter >= def_MaxFloating) return false;
        y = (y < 0 ? m_MaxCounter * def_SizeBarCaption : y);
        CreateBarTitle();
        CreateCaption(sz0);
        CreateBtnMaxMin();
        CreateRegion(TimeFrame, Scale);
        m_Win[m_MaxCounter].handle = ObjectGetInteger(Terminal.Get_ID(), m_Win[m_MaxCounter].szRegionChart, OBJPROP_CHART_ID);
        ChartApplyTemplate(m_Win[m_MaxCounter].handle, sz0 + ".tpl");   
        m_Win[m_MaxCounter].szVLine = (string)ObjectsTotal(Terminal.Get_ID(), -1, -1) + (string)MathRand();
        ObjectCreate(m_Win[m_MaxCounter].handle, m_Win[m_MaxCounter].szVLine, OBJ_VLINE, 0, 0, 0);
        ObjectSetInteger(m_Win[m_MaxCounter].handle, m_Win[m_MaxCounter].szVLine, OBJPROP_COLOR, clrBlack);
        m_Win[m_MaxCounter].PosX = -1;
        m_Win[m_MaxCounter].PosY = -1;
        m_Win[m_MaxCounter].PosX_Minimized = m_Win[m_MaxCounter].PosX_Maximized = x;
        m_Win[m_MaxCounter].PosY_Minimized = m_Win[m_MaxCounter].PosY_Maximized = y;
        SetDimension(w, h, true, m_MaxCounter);
        SetPosition(x, y, m_MaxCounter);
        ChartRedraw(m_Win[m_MaxCounter].handle);
        m_MaxCounter++;
        return true;
}

Mit diesem Code wird die notwendige Unterstützung geschaffen, damit wir die Vorlage auf das Objekt CHART anwenden können. Sie ist in dem hervorgehobenen Code-Teil implementiert. Bedenken wir, dass der einzige Parameter, der für den Aufruf dieser Funktion wirklich erforderlich ist, der Name der Vorlage ist. Alle anderen Werte sind vorinitialisiert, aber nichts hindert uns daran, selbst festzulegen, welche Werte benötigt werden. Für jedes neu erstellte Fenster wird das nächste Fenster leicht verschoben, damit es sich nicht mit anderen Fenstern überschneidet. Dies wird in der folgenden Zeile dargestellt:

y = (y < 0 ? m_MaxCounter * def_SizeBarCaption : y);


Die nächste, interessante Funktion in dieser Klasse behandelt Nachrichten. Das ist deren Code:

void DispatchMessage(int id, long lparam, double dparam, string sparam)
{
        int mx, my;
        datetime dt;
        double p;
        static int six = -1, siy = -1, sic = -1;
                                
        switch (id)
        {
                case CHARTEVENT_MOUSE_MOVE:
                        mx = (int)lparam;
                        my = (int)dparam;
                        if ((((int)sparam) & 1) == 1)
                        {
                                if (sic == -1)  for (int c0 = m_MaxCounter - 1; (sic < 0) && (c0 >= 0); c0--)
                                        sic = (((mx > m_Win[c0].PosX) && (mx < (m_Win[c0].PosX + m_Win[c0].Width)) && (my > m_Win[c0].PosY) && (my < (m_Win[c0].PosY + def_SizeBarCaption))) ? c0 : -1);
                                if (sic >= 0)
                                {
                                        if (six < 0) ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, false);
                                        six = (six < 0 ? mx - m_Win[sic].PosX : six);
                                        siy = (siy < 0 ? my - m_Win[sic].PosY : siy);
                                        SetPosition(mx - six, my - siy, sic);
                                }
                        }else
                        {
                                if (six > 0) ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, true);
                                six = siy = sic = -1;
                        }
                        ChartXYToTimePrice(Terminal.Get_ID(), mx, my, my, dt, p);
                        for (int c0 = 0; c0 < m_MaxCounter; c0++)
                                ObjectMove(m_Win[c0].handle, m_Win[c0].szVLine, 0, dt, 0);
                        break;
                case CHARTEVENT_OBJECT_CLICK:
                        for (int c0 = 0; c0 < m_MaxCounter; c0++) if (sparam == m_Win[c0].szBtnMaxMin)
                        {
                                SwapMaxMin((bool)ObjectGetInteger(Terminal.Get_ID(), m_Win[c0].szBtnMaxMin, OBJPROP_STATE), c0);
                                break;
                        }
                        break;
                case CHARTEVENT_CHART_CHANGE:
                        for(int c0 = 0; c0 < m_MaxCounter; c0++)
                        {
                               ObjectSetInteger(Terminal.Get_ID(), m_Win[c0].szRegionChart, OBJPROP_PERIOD, m_Win[c0].TimeFrame);
                               ObjectSetInteger(Terminal.Get_ID(), m_Win[c0].szRegionChart, OBJPROP_CHART_SCALE,(m_Win[c0].Scale < 0 ? ChartGetInteger(Terminal.Get_ID(),CHART_SCALE):m_Win[c0].Scale));
                        }
                        m_LimitX = (int)ChartGetInteger(Terminal.Get_ID(), CHART_WIDTH_IN_PIXELS);
                        m_LimitY = (int)ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS);
                        break;
        }
        for (int c0 = 0; c0 < m_MaxCounter; c0++)
                ChartRedraw(m_Win[c0].handle);
}


Diese Funktion implementiert die gesamte Ereignisbehandlung, die die Klasse C_ChartFloating unterstützt. Unabhängig davon, wie viele Fenster vorhanden sind, werden alle auf die gleiche Weise behandelt. Wenn wir dies innerhalb der Funktion OnChartEvent im Expert Advisor tun würden, wäre die Funktion extrem komplex und nicht sehr stabil. Und indem wir die Funktionalität hier, in der Objektklasse, implementieren, garantieren wir die Integrität des Codes. Wenn wir also keine schwebenden Fenster verwenden wollen, müssen wir nur die Datei aus der Klasse und aus den Punkten, an denen auf sie zugegriffen werden kann, entfernen. Diese Implementierung ermöglicht es Ihnen, den Code viel schneller und einfacher zu gestalten.

Es gibt auch einen interessanten Teil im obigen Code. Er ist hervorgehoben, und der interne Code lautet wie folgt:

void SwapMaxMin(const bool IsMax, const int c0)
{
        m_Win[c0].IsMaximized = IsMax;
        SetDimension((m_Win[c0].IsMaximized ? m_Win[c0].MaxWidth : 100), (m_Win[c0].IsMaximized ? m_Win[c0].MaxHeight : 0), false, c0);
        SetPosition((m_Win[c0].IsMaximized ? m_Win[c0].PosX_Maximized : m_Win[c0].PosX_Minimized), (m_Win[c0].IsMaximized ? m_Win[c0].PosY_Maximized : m_Win[c0].PosY_Minimized), c0);
}


Was bewirkt der obige Code? Sieht er zu verwirrend aus? Um den zu verstehen, sollten wir uns die folgende Animation genau ansehen.


Wenn ein schwebendes Fenster erstellt wird, hat es einen Anfangsankerpunkt, der vom Programmierer oder dem Positionierungssystem der Klasse selbst festgelegt wird. Dieser Ankerpunkt ist sowohl für das maximierte als auch für das minimierte Fenster derselbe. Diese Werte sind nicht festgelegt, d.h. der Nutzer kann diese Punkte leicht ändern.

Angenommen, wir benötigen eine bestimmte Stelle auf einem leeren Chart, dann können Sie das maximierte Fenster an eine Stelle verschieben, die leicht und schnell zu lesen ist, dann dasselbe Fenster minimieren und an eine andere Stelle verschieben, zum Beispiel in die Ecke des Bildschirms. Das System merkt sich das, und wenn wir das Fenster maximieren, springt es an den letzten Ankerpunkt, an dem es sich vor dem Minimieren befand. Dasselbe gilt für die umgekehrte Situation, wenn das Fenster minimiert wird.


Schlussfolgerung

Das ist alles für den Moment. Im nächsten Artikel werden wir diese Funktionalität auf die IDE-Unterstützungsklasse ausweiten.