Wie man den Handelsverlauf mehrerer Währungen basierend auf HTML- und CSV-Berichten visualisiert.

Stanislav Korotky | 23 Mai, 2019

Seit seiner Einführung bietet MetaTrader 5 die Möglichkeit, mehrere Währungen zu testen. Diese Möglichkeit wird von Händlern oft genutzt. Die Funktion ist jedoch nicht universell einsetzbar. Insbesondere kann der Nutzer nach der Durchführung eines Tests ein Chart mit den durchgeführten Handelsoperationen öffnen. Dies ist jedoch nur das Chart eines gehandelten Symbols, das in den Einstellungen des Strategietesters ausgewählt wurde. Die gesamte Handelshistorie aller verwendeten Symbole kann nach dem Testen nicht eingesehen werden, während die visuelle Prüfung nicht immer effizient ist. Nach einiger Zeit nach der Prüfung kann eine zusätzliche Analyse erforderlich sein. Außerdem kann ein Bericht von einer anderen Person erstellt werden. Daher wäre ein Werkzeug zur Visualisierung des Handels mit mehreren Arbeitssymbolen auf der Grundlage des HTML-Testberichts sehr nützlich.

Diese Aufgabe ist eng mit einer anderen ähnlichen MetaTrader-Anwendung verbunden. Viele der auf mql5.com verfügbaren Handelssignale betreffen den Mehrwährungshandel. Es wäre praktisch, die CSV-Dateien mit der Signalhistorie auf den Charts anzuzeigen.

Lassen Sie uns einen Indikator entwickeln, der die oben genannten Funktionen erfüllen kann.

Um die parallele Analyse mehrerer Arbeitssymbole zu ermöglichen, werden in Diagrammunterfenstern mehrere Indikatorinstanzen (eine pro Symbol) angelegt. Die wichtigsten grafischen Objekte sind die "Kurse" des ausgewählten Symbols (die sich in der Regel vom Diagrammsymbol unterscheiden), synchronisiert mit den Balken des Hauptfensters. Mit diesen "Kurse" werden die Trendlinien entsprechend den Handelsaufträgen (Positionen) gezeichnet.

Es gibt einen alternativen Ansatz: Deals werden im Hauptfenster angezeigt, aber in diesem Fall wird nur ein Symbol im Chart analysiert. Dieser Ansatz erfordert einen weiteren Indikator ohne Puffer, mit der Möglichkeit, zu jedem der im Bericht enthaltenen Symbole zu wechseln.

Der vorherige Artikel lieferte die Beschreibung eines HTML-Parsers basierend auf CSS-Selektoren[1]. Der Parser extrahiert die Liste der Deals aus dem HTML-Bericht, anhand derer wir den Handel bilden können (grafische Objekte). Das Parsen von CSV-Dateien aus dem Bereich Signale ist etwas einfacher, während das Dateiformat für die Signale MetaTrader 4 (*.history.csv) und MetaTrader 5 (*.positions.csv) durch die integrierten MQL-Funktionen unterstützt wird.

Der Indikator SubChart

Der erste Schritt in der Implementierung besteht darin, einen einfachen Indikator zu erstellen, der die "Kurse" eines externen Symbols in einem Unterfenster eines beliebigen Diagramms anzeigt. Dies ist der Indikator SubChart.

Um Daten mit OHLC-Werten (Open, High, Low, Close) anzuzeigen, bietet der MQL mehrere Darstellungsarten, einschließlich DRAW_CANDLES und DRAW_BARS. Jeder von ihnen verwendet vier Indikatorpuffer. Anstatt einen der Stile auszuwählen, werden wir beide Optionen unterstützen. Der Stil wird dynamisch basierend auf den aktuellen Fenstereinstellungen ausgewählt. Eine Gruppe von Auswahlknöpfen ist in den Chart-Einstellungen unter der Registerkarte Allgemein verfügbar: "Bars", "Japanische Kerzen" und "Linie". Für den schnellen Zugriff stehen die gleichen Schaltflächen zur Verfügung wie die Schaltflächen in der Symbolleiste. Die Einstellungen können über MQL mit dem folgenden Aufruf abgerufen werden:

(ENUM_CHART_MODE)ChartGetInteger(0, CHART_MODE)

Die Enumeration ENUM_CHART_MODE enthält Elemente mit ähnlichem Zweck: CHART_CANDLES, CHART_BARS, CHART_LINE.

Die Unterstützung für den letzten CHART_LINE-Punkt wird auch im Indikator implementiert. Dadurch wird die Indikatordarstellung entsprechend dem Hauptfenster geändert, wenn der Anzeigemodus im UI gewechselt wird. DRAW_LINE ist für den letzten Modus geeignet; er verwendet einen Puffer.

Beginnen wir mit der Implementierung. Deklarieren Sie die Anzahl der Indikatorpuffer und der angezeigten grafischen Objekttypen:

#property indicator_separate_window
#property indicator_buffers 4
#property indicator_plots   1

Hinzufügen der Eingabeparameter:

input string SubSymbol = ""; // Symbol
input bool Exact = false;

Mit Hilfe von SubSymbol ist es möglich, für den Indikator für ein anderes Symbol als das aktuelle im Hauptfenster einzustellen.

Der Parameter Exact bestimmt das Verhalten für den Fall, dass die Balken im Hauptfenster keine passenden Balken eines anderen Symbols mit genau der gleichen Zeit haben. Der Parameter wird im Funktionsaufruf iBarShift verwendet. Seine visuelle Wirkung wird wie folgt sein:

Der Parameter SubSymbol ist standardmäßig eine leere Zeichenkette. Das bedeutet, dass die Kurse die des Hauptfensters sind. In diesem Fall sollten Sie den aktuellen Variablenwert bearbeiten und auf _Symbol setzen. Da 'input' jedoch schreibgeschützte Variablen im MQL sind, müssen wir die Zwischenvariable 'Symbol' deklarieren und ihr in der Funktion OnInit den Wert zuweisen.

string Symbol;

int OnInit()
{
  Symbol = SubSymbol;
  if(Symbol == "") Symbol = _Symbol;
  else SymbolSelect(Symbol, true);
  ...

Bitte beachten Sie, dass dieses Symbol auch der Marktübersicht hinzugefügt werden sollte, da es möglicherweise nicht in der Liste der Marktübersicht sein könnte.

Verwenden Sie die Variable "mode", um den aktuellen Anzeigemodus zu steuern:

ENUM_CHART_MODE mode = 0;

Die folgenden vier Indikatorpuffer werden verwendet:

// OHLC
double open[];
double high[];
double low[];
double close[];

Ergänzen wir noch eine kleine Funktion, um die Puffer ganz normal zu initialisieren (mit den Ersteinstellungen der Eigenschaft als "Zeitreihe"):

void InitBuffer(int index, double &buffer[], ENUM_INDEXBUFFER_TYPE style)
{
  SetIndexBuffer(index, buffer, style);
  ArraySetAsSeries(buffer, true);
}

Die Initialisierung der Grafik erfolgt ebenfalls mit einer Hilfsfunktion (dieser Indikator hat nur einen grafischen Aufbau; die Funktion kann bei Verwendung mehrerer Konstruktionen Zeit sparen):

void InitPlot(int index, string name, int style, int width = -1, int colorx = -1)
{
  PlotIndexSetInteger(index, PLOT_DRAW_TYPE, style);
  PlotIndexSetDouble(index, PLOT_EMPTY_VALUE, 0);
  PlotIndexSetString(index, PLOT_LABEL, name);
  if(width != -1) PlotIndexSetInteger(index, PLOT_LINE_WIDTH, width);
  if(colorx != -1) PlotIndexSetInteger(index, PLOT_LINE_COLOR, colorx);
}

Die folgende Funktion wechselt von der Chartanzeige zur Pufferstil:

int Mode2Style(/*global ENUM_CHART_MODE mode*/)
{
  switch(mode)
  {
    case CHART_CANDLES: return DRAW_CANDLES;
    case CHART_BARS: return DRAW_BARS;
    case CHART_LINE: return DRAW_LINE;
  }
  return 0;
}

Die Funktion verwendet die obengenannte globale Variable 'mode', der ein entsprechender Wert in OnInit zugewiesen werden sollte, zusammen mit den Aufrufen aller Hilfsfunktionen.

  InitBuffer(0, open, INDICATOR_DATA);
  string title = "# Open;# High;# Low;# Close";
  StringReplace(title, "#", Symbol);
  mode = (ENUM_CHART_MODE)ChartGetInteger(0, CHART_MODE);
  InitPlot(0, title, Mode2Style());

  InitBuffer(1, high, INDICATOR_DATA);
  InitBuffer(2, low, INDICATOR_DATA);
  InitBuffer(3, close, INDICATOR_DATA);

Dies ist immer noch nicht ausreichend für das ordnungsgemäße Funktionieren des Indikators. Die Linienfarbe sollte je nach aktuellem Modus (der Modusvariablen) geändert werden: Die Farben werden von die Chart-Einstellungen übernommen.

void SetPlotColors()
{
  if(mode == CHART_CANDLES)
  {
    PlotIndexSetInteger(0, PLOT_COLOR_INDEXES, 3);
    PlotIndexSetInteger(0, PLOT_LINE_COLOR, 0, (int)ChartGetInteger(0, CHART_COLOR_CHART_LINE));  // Rechteck
    PlotIndexSetInteger(0, PLOT_LINE_COLOR, 1, (int)ChartGetInteger(0, CHART_COLOR_CANDLE_BULL)); // aufwärts
    PlotIndexSetInteger(0, PLOT_LINE_COLOR, 2, (int)ChartGetInteger(0, CHART_COLOR_CANDLE_BEAR)); // abwärts
  }
  else
  {
    PlotIndexSetInteger(0, PLOT_COLOR_INDEXES, 1);
    PlotIndexSetInteger(0, PLOT_LINE_COLOR, (int)ChartGetInteger(0, CHART_COLOR_CHART_LINE));
  }
}

Die Hinzufügung des Aufrufs von SetPlotColors() in OnInit und die Einstellung der Wertgenauigkeit garantieren die korrekte Anzeige nach dem Start.

  SetPlotColors();

  IndicatorSetString(INDICATOR_SHORTNAME, "SubChart (" + Symbol + ")");
  IndicatorSetInteger(INDICATOR_DIGITS, (int)SymbolInfoInteger(Symbol, SYMBOL_DIGITS));
  
  return INIT_SUCCEEDED;
}

Wenn der Benutzer jedoch den Chart-Modus ändert, während der Indikator läuft, ist es notwendig, dieses Ereignis zu verfolgen und die Eigenschaften der Puffer zu ändern. Dies wird durch die Funktion OnChartEvent durchgeführt.

void OnChartEvent(const int id,
                  const long& lparam,
                  const double& dparam,
                  const string& sparam)
{
  if(id == CHARTEVENT_CHART_CHANGE)
  {
    mode = (ENUM_CHART_MODE)ChartGetInteger(0, CHART_MODE);
    PlotIndexSetInteger(0, PLOT_DRAW_TYPE, Mode2Style());
    SetPlotColors();
    ChartRedraw();
  }
}

Nun müssen wir die wichtigste Indikatorfunktion in der Funktion OnCalculate schreiben. Das Besondere dieser Funktion ist, dass der Indikator anstelle des Chartsymbols tatsächlich Kurse von Drittanbietern verwendet. Daher sind alle Standardprogrammiertechniken, die auf den Werten rates_total und prev_calculated basieren und normalerweise vom Kernel übergeben werden, nicht oder nur teilweise geeignet. Kurse der Symbole von Drittanbietern werden asynchron heruntergeladen, so dass jederzeit eine neue Anzahl von Balken "ankommen" kann - diese Situation erfordert eine vollständige Neuberechnung. Daher sollten Sie Variablen erstellen, die die Anzahl der Balken auf diesem Symbol (lastAvailable) und den editierbaren "Klon" der Konstanten prev_calculated Argument steuern.

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime& time[],
                const double& op[],
                const double& hi[],
                const double& lo[],
                const double& cl[],
                const long& tick_volume[],
                const long& volume[],
                const int& spread[])
{
  static int lastAvailable = 0;
  static bool initialized = false;

  int _prev_calculated = prev_calculated;

  if(iBars(Symbol, _Period) - lastAvailable > 1) // Lücke in den Bars
  {
    _prev_calculated = 0;
    lastAvailable = 0;
    initialized = false;
  }

  if(_prev_calculated == 0)
  {
    for(int i = 0; i < rates_total; ++i)
    {
      open[i] = 0;
      high[i] = 0;
      low[i] = 0;
      close[i] = 0;
    }
  }

Wenn sich das Indikatorsymbol vom Fenstersymbol unterscheidet, verwenden Sie die Funktion iBarShift, um synchrone Balken zu finden und deren OHLC-Werte zu kopieren.

  if(_Symbol != Symbol)
  {
    for(int i = 0; i < MathMax(rates_total - _prev_calculated, 1); ++i)
    {
      datetime dt = iTime(_Symbol, _Period, i);
      int x = iBarShift(Symbol, _Period, dt, Exact);
      if(x != -1)
      {
        open[i] = iOpen(Symbol, _Period, x);
        high[i] = iHigh(Symbol, _Period, x);
        low[i] = iLow(Symbol, _Period, x);
        close[i] = iClose(Symbol, _Period, x);
      }
    }
  }

Wenn das Indikatorsymbol mit dem Fenstersymbol übereinstimmt, verwenden Sie einfach die übergebenen Arrays:

  else
  {
    ArraySetAsSeries(op, true);
    ArraySetAsSeries(hi, true);
    ArraySetAsSeries(lo, true);
    ArraySetAsSeries(cl, true);
    for(int i = 0; i < MathMax(rates_total - _prev_calculated, 1); ++i)
    {
      open[i] = op[i];
      high[i] = hi[i];
      low[i] = lo[i];
      close[i] = cl[i];
    }
  }

Schließlich müssen wir das Hochladen von Daten bereitstellen. Lassen Sie uns die Implementierung der Funktion RefreshHistory aus dem Beispiel von MetaQuotes verwenden (wir werden diesen Code als Header-Datei Refresh.mqh einbinden).

Die 'static' Variable 'initialized' enthält ein Zeichen für den Abschluss der Aktualisierung. Setzen Sie es auf true, wenn RefreshHistory einen Erfolg meldet oder wenn die Anzahl der Symbolleisten von Drittanbietern konstant und ungleich Null bleibt (falls es keine Historie für die benötigte Anzahl von Balken gibt).

  if(lastAvailable == iBars(Symbol, _Period) && lastAvailable != 0)
  {
    if(!initialized)
    {
      Print("Updated ", Symbol, " ", iBars(Symbol, _Period), " bars");
      initialized = true;
    }
    return rates_total;
  }

  if(!initialized)
  {
    if(_Symbol != Symbol)
    {
      Print("Updating ", Symbol, " ", lastAvailable, " -> ", iBars(Symbol, _Period), " bars up to ", (string)time[0], "... Please wait");
      int result = RefreshHistory(Symbol, time[0]);
      if(result >= 0 && result <= 2)
      {
        _prev_calculated = rates_total;
      }
      if(result >= 0)
      {
        initialized = true;
        ChartSetSymbolPeriod(0, _Symbol, _Period);
      }
    }
    else
    {
      initialized = true;
    }
  }
  
  lastAvailable = iBars(Symbol, _Period);
  
  return _Symbol != Symbol ? _prev_calculated : rates_total;
}

Wenn das Laden der Daten zu lange dauert, kann die manuelle Aktualisierung des Charts erforderlich sein.

Nach Abschluss der Initialisierung werden die neuen Balken im ressourcenschonenden Modus berechnet, d.h. unter Berücksichtigung von _prev_calculated und rates_total. Wenn sich die Anzahl der zuletzt verfügbaren Balken um mehr als 1 ändert, sollten alle Balken neu gezeichnet werden. Zu diesem Zweck sollte 'initialized' auf falsch zurückgesetzt werden.

Fügen Sie den Indikator einem Chart hinzu, sagen wir EURUSD und geben Sie ein anderes Symbol in die Parameter ein, z.B. UKBrent (Brent CFD; es ist interessant, denn wenn 'Exact' wahr ist, ist das Fehlen von Nachtbars in den Zeitfenstern innerhalb des Tages offensichtlich). Klicken Sie auf die Schaltflächen für den Moduswechsel und vergewissern Sie sich, dass die Indikatordarstellung korrekt ist.

Umschalten des Anzeigemodus des Indikators

Umschalten des Anzeigemodus des Indikators

Beachten Sie, dass bei der Darstellung des Indikators als Linie der erste Puffer (Index 0) open (Eröffnungskurse) verwendet wird. Dies unterscheidet sich vom Haupt-Chart, das auf close (Schlusskurse) basiert. Dieser Ansatz ermöglicht es, ein vollständiges Neuzeichnen der Indikatoren beim Wechsel zur oder von der linearen Darstellung zu vermeiden: Um die auf dem Schlusskurs basierende Linie anzuzeigen, müssen die Schlusskurse in den ersten (den einzigen) Puffer kopiert werden, während im Kerzen- und Bar-Modus (wenn 4 Puffer verwendet werden) die ersten Puffer die Eröffnungspreise speichern. Die aktuelle Implementierung basiert auf der Tatsache, dass der Puffer für open der erste in OHLC ist, und daher führt eine Stiländerung sofort zu einer Änderung der Darstellung ohne Neuberechnung. Der Linienmodus wird in der Historienanalyse kaum genutzt, daher ist diese Funktion nicht kritisch.

Starten Sie den Indikator im visuellen Testmodus aus.

Der Indikator SubChart im visuellen Testmodus

Der Indikator SubChart im visuellen Testmodus

Nun können wir mit der Visualisierung der Handelsgeschichte fortfahren, die auf diesem Indikator basiert.

Der Indikator SubChartReporter

Nennen wir den neuen Indikator SubChartReporter. Fügen wir dem zuvor erstellten Code noch die Möglichkeit hinzu, HTML- und CSV-Berichte zu lesen. Der Name der Datei für die Analyse wird in der Eingabevariablen ReportFile übergeben. Lassen Sie uns auch Eingabeparameter für die Angabe der Zeitverschiebung sowie für die Angabe von Symbolpräfixen und -suffixen bereitstellen, wenn ein Bericht von einem anderen Benutzer (aus einer anderen Handelsumgebung) empfangen wird.

input string ReportFile = ""; // · ReportFile
input string Prefix = ""; // · Prefix
input string Suffix = ""; // · Suffix
input int  TimeShift = 0; // · TimeShift

Spezielle Klassen im Indikator SubChartReporter verarbeiten den Empfang von Daten und die Generierung von grafischen Objekten (Trendlinien).

Da wir HTML zusammen mit CSV-Dateien analysieren wollen, wurde eine allgemeine Basisklasse für alle Berichtstypen - Processor - entwickelt. Spezifische Implementierungen für HTML und CSV wurden davon übernommen: ReportProzessor und HistoryProzessor.

Die folgenden Variablen sind im Abschnitt Prozessor beschrieben:

class Processor
{
  protected:
    string symbol;
    string realsymbol;
    IndexMap *data;
    ulong timestamp;
    string prefix;

Um das Umschalten zwischen verschiedenen Berichtssymbolen zu ermöglichen, fügen wir Schaltflächen für die Schnittstelle hinzu. Die aktuell gedrückte Tasten-ID (entspricht dem ausgewählten Symbol) wird in einer Variablen gespeichert:

    string pressed;

Die folgenden Hilfsmethoden zur Objekterzeugung sind in der Prozessorklasse implementiert:

    void createTrend(const long dealIn, const long dealOut, const int type, const datetime time1, const double price1, const datetime time2, const double price2, const string &description)
    void createButton(const int x, const int y, const int dx, const int dy, const string text, const bool selected)
    void controlPanel(const IndexMap &symbols)

createTrend erstellt die Darstellung für einen separaten Trade (Trendlinie und zwei Pfeile), createButton erstellt eine funktionierende Symbolschaltfläche, controlPanel ist der komplette Satz von Schaltflächen für alle im Bericht dargestellten Symbole. Die Schaltflächen werden in der linken unteren Ecke des Subfensters angezeigt.

Das 'public' Interface der Klasse Processor umfasst zwei Gruppen von Methoden:

    virtual IndexMap *load(const string file) = 0;
    virtual int getColumnCount() = 0;
    virtual int getSymbolColumn() = 0;
    virtual datetime getStart() = 0;
    virtual bool applyInit() { return true; }
    virtual void makeTrade(IndexMap *row) = 0;
    virtual int render() = 0;

    bool attach(const string file)
    bool apply(const string _s = NULL)
    bool isEmpty() const
    string findrealsymbol()
    string _symbol() const
    string _realsymbol() const
    void onChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)

Der vollständige Quellcode ist unten angehängt. Ich möchte hier nur eine kurze Beschreibung geben.

Die letzten beiden Methoden existieren separat, da einige Berichtsformate, wie z.B. HTML-Berichte in MetaTrader 5, Aufzeichnungen von Geschäften enthalten, wir jedoch Positionen anzeigen müssen. Daher benötigen wir eine zusätzliche Konvertierung von einer Art von Entitäten in eine andere, mit der Notwendigkeit, eine Reihe von Geschäften anzuzeigen.

Nicht-virtuelle Methoden werden im Folgenden in vereinfachter Form dargestellt. Die Methode attach erhält den Namen der zu analysierenden Datei, lädt sie mit der virtuellen Methode 'load', erstellt eine Liste eindeutiger Symbole und erstellt ein 'panel' der Schaltflächen für diese Symbole.

    bool attach(const string file)
    {
      data = load(file);
      
      IndexMap symbols;
      
      for(int i = 0; i < data.getSize(); ++i)
      {
        IndexMap *row = data[i];
        // Sammeln aller Symbole, jedes nur einmal
        string s = row[getSymbolColumn()].get<string>();
        StringTrimLeft(s);
        if(StringLen(s) > 0) symbols.set(s);
      }

      if(symbols.getSize() > 0)
      {
        controlPanel(symbols);
      }
      return true;
    }

Die Methode 'apply' aktiviert im Indikator das ausgewählte Arbeitssymbol aus den im Bericht verfügbaren. Zuerst werden alte Objekte aus dem Chart gelöscht (falls vorhanden), dann wird nach einem passenden realen Symbol gesucht (z.B. EURUSD von Ihrem Broker anstelle von EURUSD.m, das im Bericht angegeben ist). Hier können abgeleitete Klassen alte interne Arrays mit applyInit zurücksetzen. Dann werden die Handelspositionen basierend auf Tabelleneinträgen mit dem passenden Symbol (Call von makeTrade) gebildet. Auf Basis dieser Handelspositionen werden grafische Objekte gezeichnet (Call of Render) und die Oberfläche aktualisiert (aktive Schaltflächenauswahl, Namensänderung des Indikators, Aufruf von ChartRedraw).

                                                                                                                                          
    bool apply(const string _s = NULL)
    {
      ObjectsDeleteAll(0, "SCR", ChartWindowFind(), OBJ_TREND);

      if(_s != NULL && _s != "") symbol = _s;
      if(symbol == NULL)
      {
        Print("No symbol selected");
        return false;
      }
      
      string real = findrealsymbol();
      if(real == NULL)
      {
        Print("No suitable symbol found");
        return false;
      }
      
      SymbolSelect(real, true);

      if(!applyInit()) return false;
      
      int selected = 0;
  
      for(int i = 0; i < data.getSize(); ++i)
      {
        IndexMap *row = data[i];
        
        string s = row[getSymbolColumn()].get<string>();
        StringTrimLeft(s);
        
        if(s == symbol)
        {
          selected++;
          makeTrade(row);
        }
      }
      
      pressed = prefix + "#" + symbol;
      ObjectSetInteger(0, pressed, OBJPROP_BGCOLOR, clrGreen);
      
      int trends = render();
      Print(data.getSize(), " records in total");
      Print(selected, " trades for ", symbol);
      
      string title = CHART_REPORTER_TITLE + " (" + symbol + ", " + (string)selected + " records, " + (string)trends + " trades)";
      IndicatorSetString(INDICATOR_SHORTNAME, title);
      
      ChartRedraw();
      return true;
    }

Die Methode findrealsymbol verwendet mehrere Ansätze zur Suche nach übereinstimmenden Symbolen. Sie prüft die Verfügbarkeit von Marktdaten (Bid-Preisen) für das Symbol. Wenn die Daten gefunden werden, gilt das Symbol als echt. Wenn keine Marktdaten vorhanden sind, versucht das Programm, die Parameter Suffix und/oder Präfix abzugleichen (wenn das Suffix oder Präfix angegeben ist), d.h. es fügt sie dem Symbolnamen hinzu oder entfernt sie. Wenn der Preis nach der Änderung eingeht, bedeutet dies, dass ein gültiger Alias für das Symbol gefunden wurde.

Die Methoden _symbol und _realsymbol geben den Namen des aktuellen Symbols aus dem Bericht zurück und geben dieses Symbol oder sein gültiges Gegenstück für Ihr Konto zurück. Bei der Analyse Ihrer eigenen Berichte erhalten Sie den gleichen Symbolnamen, es sei denn, der Broker schließt Ihr Symbol aus.

Die Methode onChartEvent behandelt OnChartEvent, d.h. Ereignisse, die mit einem Tastenklick ausgelöst werden. Die zuvor ausgewählte Schaltfläche (falls vorhanden) wird wie alle anderen Schaltflächen grau. Der wichtige Teil ist der Aufruf der virtuellen Methode 'apply', an die der Name des neuen Symbols übergeben wird, das aus der Tastenkennung extrahiert wurde.

    void onChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
    {
      if(id == CHARTEVENT_OBJECT_CLICK)
      {
        int x = StringFind(sparam, "_#");
        if(x != -1)
        {
          string s = StringSubstr(sparam, x + 2);
          Print(s, " ", sparam, " ", pressed);
          
          ObjectSetInteger(0, sparam, OBJPROP_STATE, false);
          ObjectSetInteger(0, pressed, OBJPROP_STATE, false);
          ObjectSetInteger(0, pressed, OBJPROP_BGCOLOR, clrGray);
          pressed = "";
          
          if(apply(s)) // setzt einen Klick und andere Eigenschaften
          {
            ChartSetSymbolPeriod(0, _Symbol, _Period);
          }
        }
      }
    }

Kommen wir nun zur Implementierung der Hauptfunktionen in den virtuellen Methoden der abgeleiteten Klassen. Beginnen wir mit dem ReportProzessor.

Die HTML-Seiten werden mit dem WebDataExtractor aus Artikel[1] analysiert. Der Parser wird als Include-Datei WebDataExtractor.mqh verwendet. Im Vergleich zum ursprünglichen Quellcode sind alle Arbeitsmethoden in dieser Datei in der Klasse HTMLConverter verpackt, um den globalen Kontext nicht zu beeinträchtigen. Uns interessiert vor allem die Methode HTMLConverter::convertReport2Map, die fast vollständig mit der in[1] beschriebenen Funktion 'process' übereinstimmt. Der Name der Berichtsdatei aus ReportFile wird in convertReport2Map eingegeben. Die Funktion gibt IndexMap zurück, mit Zeichenketten, die den Handelsoperationen in der Berichtstabelle entsprechen.

Alle für das Parsen von HTML-Berichten erforderlichen Einstellungen, wie RowSelector und ColumnSettingsFile, sind bereits in der Header-Datei angegeben. Sie können jedoch bearbeitet werden, da sie als Eingabeparameter bezeichnet werden. Die Standardparameter gelten für die Berichte von MetaTrader 5, aus denen eine Tabelle der Geschäfte analysiert und weitere angezeigte Positionen basierend auf diesen Geschäften berechnet werden. Jedes Geschäft wird durch eine Instanz der speziellen Klasse 'Deal' beschrieben. Der Konstruktor 'Deal' erhält einen Eintrag aus der Berichtstabelle, verpackt in IndexMap. Die folgenden Felder werden in 'Deal' gespeichert: Deal-Zeit und Preis, Art und Richtung, Volumen und andere Eigenschaften.

class ReportProcessor: public Processor
{
  private:
    class Deal   // wenn MQL5 die Spezifikation des privaten Zugriffs für Klassen respektieren würde,
    {            // würden Handelsgeschäft unerreichbar von außerhalb werden, daher wäre es in Ordnung,
      public:    // Felder für den direkten Zugriff nur vom Prozessor als 'public' zugänglich zu machen.
        datetime time;
        double price;
        int type;      // +1 - Kauf, -1 - Verkauf
        int direction; // +1 - rein, -1 - raus, 0 - rein/raus
        double volume;
        double profit;
        long deal;
        long order;
        string comment;
        
      public:
        Deal(const IndexMap *row) // dies ist ein MT5-Deal
        {
          time = StringToTime(row[COLUMN_TIME].get<string>()) + TimeShift;
          price = StringToDouble(row[COLUMN_PRICE].get<string>());
          string t = row[COLUMN_TYPE].get<string>();
          type = t == "buy" ? +1 : (t == "sell" ? -1 : 0);
          t = row[COLUMN_DIRECTION].get<string>();
          direction = 0;
          if(StringFind(t, "in") > -1) ++direction;
          if(StringFind(t, "out") > -1) --direction;
          volume = StringToDouble(row[COLUMN_VOLUME].get<string>());
          t = row[COLUMN_PROFIT].get<string>();
          StringReplace(t, " ", "");
          profit = StringToDouble(t);
          deal = StringToInteger(row[COLUMN_DEAL].get<string>());
          order = StringToInteger(row[COLUMN_ORDER].get<string>());
          comment = row[COLUMN_COMMENT].get<string>();
        }
    
        bool isIn() const
        {
          return direction >= 0;
        }
        
        bool isOut() const
        {
          return direction <= 0;
        }
        
        bool isOpposite(const Deal *t) const
        {
          return type * t.type < 0;
        }
        
        bool isActive() const
        {
          return volume > 0;
        }
    };

Alle Deals werden zu 'array' hinzugefügt. Im Rahmen der Analyse der Historien werden die Markteintritte in die Warteschlange aufgenommen und aus ihr entfernt, wenn ein entsprechender gegenteiliger Exit gefunden wird. Wenn die Warteschlange nach dem Durchlaufen der gesamten Historie nicht leer ist, gibt es eine offene Position.

    RubbArray<Deal *> array;
    RubbArray<Deal *> queue;

Die Klasse RubbArray ist ein Wrapper für das dynamische Array, der automatisch erweitert wird, um eingehende Daten anzupassen.

Die Implementierung einiger virtueller Methoden ist nachfolgend dargestellt:

    virtual IndexMap *load(const string file) override
    {
      return HTMLConverter::convertReport2Map(file, true);
    }

    virtual int getColumnCount() override
    {
      return COLUMNS_COUNT;
    }

    virtual int getSymbolColumn() override
    {
      return COLUMN_SYMBOL;
    }

Die letzten beiden Methoden verwenden Makrodefinitionen für die Standardtabelle der Deals aus dem HTML-Bericht MetaTrader 5.

#define COLUMNS_COUNT 13
#define COLUMN_TIME 0
#define COLUMN_DEAL 1
#define COLUMN_SYMBOL 2
...

Die Methode applyInit löscht die Arrays 'array' und 'queue'

    virtual bool applyInit() override
    {
      ((BaseArray<Deal *> *)&queue).clear();
      array.clear();
      return true;
    }

Die Objekte der Deals werden erstellt und in der Methode makeTrade 'array' zugewiesen.

    virtual void makeTrade(IndexMap *row) override
    {
      array << new Deal(row);
    }

Der interessanteste und auch schwierigste Teil ist schließlich die Analyse der Liste der Deals und die Generierung von Handelsobjekten auf deren Basis.

    virtual int render() override
    {
      int count = 0;
      
      for(int i = 0; i < array.size(); ++i)
      {
        Deal *current = array[i];
        
        if(!current.isActive()) continue;
        
        if(current.isOut())
        {
          // erster Versuch, das Passende zu finden 
          for(int j = 0; j < queue.size(); ++j)
          {
            if(queue[j].isIn() && queue[j].isOpposite(current) && queue[j].volume == current.volume)
            {
              string description;
              StringConcatenate(description, (float)queue[j].volume, "[", queue[j].deal, "/", queue[j].order, "-", current.deal, "/", current.order, "] ", (current.profit < 0 ? "-" : ""), current.profit, " ", current.comment);
              createTrend(queue[j].deal, current.deal, queue[j].type, queue[j].time, queue[j].price, current.time, current.price, description);
              current.volume = 0;
              queue >> j; // aus der Warteschlange entfernen
              ++count;
              break;
            }
          }

          if(!current.isActive()) continue;
          
          // zweiter Versuch eines teilweisen Schließens
          for(int j = 0; j < queue.size(); ++j)
          {
            if(queue[j].isIn() && queue[j].isOpposite(current))
            {
              string description;
              if(current.volume >= queue[j].volume)
              {
                StringConcatenate(description, (float)queue[j].volume, "[", queue[j].deal, "/", queue[j].order, "-", current.deal, "/", current.order, "] ", (current.profit < 0 ? "-" : ""), current.profit, " ", current.comment);
                createTrend(queue[j].deal, current.deal, queue[j].type, queue[j].time, queue[j].price, current.time, current.price, description);

                current.volume -= queue[j].volume;
                queue[j].volume = 0;
                ++count;
              }
              else
              {
                StringConcatenate(description, (float)current.volume, "[", queue[j].deal, "/", queue[j].order, "-", current.deal, "/", current.order, "] ", (current.profit < 0 ? "-" : ""), current.profit, " ", current.comment);
                createTrend(queue[j].deal, current.deal, queue[j].type, queue[j].time, queue[j].price, current.time, current.price, description);

                queue[j].volume -= current.volume;
                current.volume = 0;
                ++count;
                break;
              }
            }
          }
          
          // alle inaktiven Elemente aus der Warteschlange entfernen
          for(int j = queue.size() - 1; j >= 0; --j)
          {
            if(!queue[j].isActive())
            {
              queue >> j;
            }
          }
        }
        
        if(current.isActive()) // wenn _immer_noch_ aktiv
        {
          if(current.isIn())
          {
            queue << current;
          }
        }
      }
      
      if(!isQueueEmpty())
      {
        Print("Warning: not all deals are processed (probably, open positions left).");
      }
      
      return count;
    }

Der Algorithmus durchläuft die Liste aller Geschäfte und stellt die Markteintrittsdeals in die Warteschlange. Wenn eine Exit-Deal gefunden wird, wird in der Warteschlange nach einer geeigneten Gegenseite mit der passenden Größe gesucht. Wenn keine Position mit der exakt passenden Größe gefunden wird, werden die Volumina der Eröffnungs-Deals konsequent nach der FIFO-Regel ausgewählt, um das Exit-Volumen abzudecken. Deals mit dem vollständig abgedeckten Volumen werden aus der Warteschlange entfernt. Für jede Kombination von In- und Out-Volumen wird eine Trendlinie (createTrend) erstellt.

Die FIFO-Regel wird verwendet, da sie aus algorithmischer Sicht die sinnvollste ist. Es sind aber auch andere Optionen möglich. Ein bestimmter Handelsroboter kann die Deals nicht nur nach FIFO, sondern auch nach LIFO-Regel oder sogar in einer beliebigen Reihenfolge schließen. Gleiches gilt für den manuellen Handel. Um eine Übereinstimmung zwischen dem Eröffnungs- und Exit-Deal im Hedging-Modus herzustellen, ist es daher notwendig, einen bestimmten "Workaround" zu finden, wie beispielsweise eine Gewinnanalyse oder Kommentare. Ein Beispiel für die Gewinnberechnung zwischen zwei Preispunkten finden Sie in meinem Blogbeitrag, der jedoch keine Berücksichtigung von Kursdifferenzen beinhaltet. Diese Aufgabe ist in der Regel nicht ganz einfach und wird daher in diesem Artikel nicht behandelt. 

Daher haben wir überprüft, wie der HTML-Bericht in den beschriebenen Klassen verarbeitet wird. Die Implementierung der Klasse HistoryProcessor ist für CSV-Dateien viel einfacher. Sie ist aus den beigefügten Quellcodes leicht verständlich. Beachten Sie, dass CSV-Dateien mit der Signalhistorie von mql5.com eine unterschiedliche Anzahl von Spalten für MT4 und MT5 haben. Das entsprechende Format wird aufgrund der doppelten Erweiterung automatisch ausgewählt: ".history.csv" für MetaTrader 4 und ".positions.csv" für MetaTrader 5. Die einzige Einstellung für CSV-Dateien ist das Trennzeichen (das Standardzeichen in den mql5.com-Signaldateien ist ';').

Der wichtigste Quellcode von SubChartReporter wird von SubChart übernommen, mit dem wir die Anzeige von Drittanbieter-Angeboten in Subfenstern überprüft haben. Lassen Sie uns also auf neue Fragmente eingehen.

Klassenobjekte werden in der Ereignisbehandlung erstellt und verwendet. Der Prozessor wird in OnInit erstellt und in OnDeinit wieder gelöscht:

Processor *processor = NULL;

int OnInit()
{
  if(StringFind(ReportFile, ".htm") > 0)
  {
    processor = new ReportProcessor();
  }
  else if(StringFind(ReportFile, ".csv") > 0)
  {
    processor = new HistoryProcessor();
  }
  string Symbol = SubSymbol;
  if(Symbol == "") Symbol = _Symbol;
  else SymbolSelect(Symbol, true);
  processor.apply(Symbol);
  ...
}

void OnDeinit(const int reason)
{
  if(processor != NULL) delete processor;
}

Die Funktion OnChartEvent wurde um eine grafische Objektereignisbehandlung erweitert:

void OnChartEvent(const int id,
                  const long& lparam,
                  const double& dparam,
                  const string& sparam)
{
  if(id == CHARTEVENT_CHART_CHANGE)
  {
    ... // der gleiche Code
  }
  else
  {
    processor.onChartEvent(id, lparam, dparam, sparam);
  }
}

In der Funktion OnCalculate müssen wir die Situation verfolgen, in der sich das analysierte Symbol als Reaktion auf Benutzeraktionen geändert hat, woraufhin die vollständige Neuberechnung durchgeführt wird:

  string Symbol = processor._realsymbol();
  if(Symbol == NULL) Symbol = _Symbol;
  if(lastSymbol != Symbol)
  {
    _prev_calculated = 0;
    lastAvailable = 0;
    initialized = false;
    IndicatorSetInteger(INDICATOR_DIGITS, (int)SymbolInfoInteger(Symbol, SYMBOL_DIGITS));
  }

Wenn die erste Zeichnung abgeschlossen ist und die Anzahl der verfügbaren Balken des Arbeitssymbols stabil wird, starten Sie den Timer, um die Berichtsdaten zu laden.

  if(lastAvailable == iBars(Symbol, _Period) && lastAvailable != 0)
  {
    if(!initialized)
    {
      Print("Updated ", Symbol, " ", iBars(Symbol, _Period), " bars");
      initialized = true;
      if(ReportFile != "") //
      {                    //
        EventSetTimer(1);  //
      }                    //
    }
    
    return rates_total;
  }

Laden Sie den Bericht in den Timer-Handler hoch (falls er noch nicht hochgeladen wurde) und aktivieren Sie das ausgewählte Zeichen (es wird über die Parameter in OnInit oder durch einen Tastenklick in OnChartEvent eingestellt).

void OnTimer()
{
  EventKillTimer();
  
  if(processor.isEmpty()) // laden der Dateien nur einmal
  {
    if(processor.attach(ReportFile))
    {
      processor.apply(/*keep already selected symbol*/);
    }
    else
    {
      Print("File loading failed: ", ReportFile);
    }
  }
}

Lassen Sie uns jetzt die Leistung des Indikators testen. Öffnen Sie das EURUSD-Chart, starten Sie den Indikator darauf und geben Sie im Parameter ReportFile die Datei ReportTester-example.html (angehängt) an. Nach der Initialisierung enthält das Subfenster das EURUSD-Chart (da der Symbolparameter leer ist) und eine Reihe von Schaltflächen mit den Namen aller im Bericht enthaltenen Symbole.

Der Indikator SubChartReporter ohne ausgewähltes Arbeitssymbol

Der Indikator SubChartReporter ohne ausgewähltes Arbeitssymbol

Da der Bericht keine EURUSD beinhaltet, sind alle Schaltflächen grau. Wenn Sie auf eine beliebige Schaltfläche, z.B. EURGBP, klicken, werden diese Währungsnotierungen in das Nebenfenster geladen und entsprechende Deals angezeigt. Die Taste wird grün.

Der Indikator SubChartReporter mit einem ausgewählten Arbeitssymbol.

Der Indikator SubChartReporter mit einem ausgewählten Arbeitssymbol.

In der aktuellen Implementierung wird die Reihenfolge der Schaltflächen durch die Chronologie des Symbolauftretens im Bericht bestimmt. Bei Bedarf kann sie in beliebiger Reihenfolge sortiert werden, z.B. alphabetisch oder nach der Anzahl der Deals.

Durch Umschalten der Schaltflächen können wir alle Symbole aus dem Bericht anzeigen. Aber das ist nicht sehr praktisch. Für einige Berichte wäre es besser, alle Symbole auf einmal zu betrachten, jedes Symbol in einem eigenen Subfenster. Zu diesem Zweck schreiben wir das Skript SubChartsBuilder, das Subfenster erstellt und in diesen Subfenstern Instanzen von SubChartReporter für verschiedene Symbole startet.

Das Skript SubChartsBuilder

Das Skript hat den gleichen Parametersatz wie der Indikator SubChartReporter. Um einen einzelnen Start zu ermöglichen, werden Parameter zum Array MqlParam hinzugefügt und dann IndicatorCreate aus der MQL-API aufgerufen. Dies geschieht in einer Anwendungsfunktion createIndicator.

bool createIndicator(const string symbol)
{
  MqlParam params[18] =
  {
    {TYPE_STRING, 0, 0.0, "::Indicators\\SubChartReporter.ex5"},
    
    {TYPE_INT, 0, 0.0, NULL}, // Charteinstellungen
    {TYPE_STRING, 0, 0.0, "XYZ"},
    {TYPE_BOOL, 1, 0.0, NULL},
    
    {TYPE_INT, 0, 0.0, NULL}, // allgemeine Einstellungen
    {TYPE_STRING, 0, 0.0, "HTMLCSV"},
    {TYPE_STRING, 0, 0.0, "PREFIX"},
    {TYPE_STRING, 0, 0.0, "SUFFIX"},
    {TYPE_INT, 0, 0.0, NULL}, // Zeitversatz

    {TYPE_INT, 0, 0.0, NULL}, // html-Einstellungen
    {TYPE_STRING, 0, 0.0, "ROW"},
    {TYPE_STRING, 0, 0.0, "COLUMNS"},
    {TYPE_STRING, 0, 0.0, "SUBST"},
    {TYPE_BOOL, 0, 0.0, NULL},
    {TYPE_BOOL, 0, 0.0, NULL},
    {TYPE_BOOL, 0, 0.0, NULL},

    {TYPE_INT, 0, 0.0, NULL}, // csv-Einstellungen
    {TYPE_STRING, 0, 0.0, ";"}
  };
  
  params[2].string_value = symbol;
  params[5].string_value = ReportFile;
  params[6].string_value = Prefix;
  params[7].string_value = Suffix;
  params[8].integer_value = TimeShift;
  params[10].string_value = RowSelector;
  params[11].string_value = ColumnSettingsFile;
  params[12].string_value = SubstitutionSettingsFile;
  params[17].string_value = CSVDelimiter;
  
  int handle = IndicatorCreate(_Symbol, _Period, IND_CUSTOM, 18, params);
  if(handle == INVALID_HANDLE)
  {
    Print("Can't create SubChartReporter for ", symbol, ": ", GetLastError());
    return false;
  }
  else
  {
    if(!ChartIndicatorAdd(0, (int)ChartGetInteger(0, CHART_WINDOWS_TOTAL), handle))
    {
      Print("Can't attach SubChartReporter for ", symbol, ": ", GetLastError());
      return false;
    }
  }
  return true;
}

Bitte beachten Sie, dass der Indikator der Ressource entnommen wird, für die er im Quellcode vorregistriert ist, mit dem folgenden Befehl:

#resource "\\Indicators\\SubChartReporter.ex5"

Somit ist das Skript ein vollwertiges, unabhängiges Programm, das nicht von der Anwesenheit des Indikators bei einem bestimmten Nutzer abhängt.

Es gibt noch einen anderen Zweck dieses Ansatzes. Wenn für alle im Bericht verwendeten Symbole Kennzeicheninstanzen erzeugt werden, sind keine Steuertasten mehr erforderlich. Leider bietet MQL keine Möglichkeit zu bestimmen, ob der Indikator vom Benutzer oder über IndicatorCreate gestartet wird, ob er von selbst läuft oder ein integraler (abhängiger) Teil eines größeren Programms ist. Wenn der Indikator in einer Ressource platziert ist, ist es möglich, Schaltflächen abhängig vom Indikatorpfad ein- oder auszublenden: Der Pfad in der Ressource (d.h. das Vorhandensein der Zeichenkette "::Indicators\\") bedeutet, dass die Anzeige von Schaltflächen deaktiviert werden sollte.

Um die Funktion createIndicator für jedes Berichtssymbol aufzurufen, müssen wir den Bericht im Skript analysieren.

int OnStart()
{
  IndexMap *data = NULL;
  int columnsCount = 0, symbolColumn = 0;
  
  if(ReportFile == "")
  {
    Print("cleanUpChart");
    return cleanUpChart();
  }
  else if(StringFind(ReportFile, ".htm") > 0)
  {
    data = HTMLConverter::convertReport2Map(ReportFile, true);
    columnsCount = COLUMNS_COUNT;
    symbolColumn = COLUMN_SYMBOL;
  }
  else if(StringFind(ReportFile, ".csv") > 0)
  {
    data = CSVConverter::ReadCSV(ReportFile);
    if(data != NULL && data.getSize() > 0)
    {
      IndexMap *row = data[0];
      columnsCount = row.getSize();
      symbolColumn = CSV_COLUMN_SYMBOL;
    }
  }
  
  if(data != NULL)
  {
    IndexMap symbols;
    
    for(int i = 0; i < data.getSize(); ++i)
    {
      IndexMap *row = data[i];
      if(CheckPointer(row) == POINTER_INVALID || row.getSize() != columnsCount) break;
      
      string s = row[symbolColumn].get<string>();
      StringTrimLeft(s);
      if(StringLen(s) > 0) symbols.set(s);
    }
    
    for(int i = 0; i < symbols.getSize(); ++i)
    {
      createIndicator(symbols.getKey(i));
    }
    delete data;
  }

  return 0;
}

Wenn das Skript mit einem leeren Berichtsnamen ausgeführt wird, werden alle Unterfenster mit Indikatorinstanzen aus dem Fenster entfernt (falls sie zuvor erstellt wurden). Dies geschieht über die Funktion cleanUpChart.

bool cleanUpChart()
{
  bool result = true;
  int n = (int)ChartGetInteger(0, CHART_WINDOWS_TOTAL);
  for(int i = n - 1; i > 0; --i)
  {
    string name = ChartIndicatorName(0, i, 0);
    if(StringFind(name, "SubChartReporter") == 0)
    {
      Print("Deleting ", name);
      result &= ChartIndicatorDelete(0, i, name);
    }
  }
  return result;
}

Dies ist eine effiziente Möglichkeit, das Diagramm nach Abschluss der Berichtsanalyse zu löschen.

Um das Skript zu testen, habe ich CSV-Dateien mit Signalhistorie heruntergeladen. Hier ist, wie es aussehen könnte (das Hauptdiagramm wird minimiert):

Mehrere Instanzen von SubChartReporter analysieren den Mehrwährungshandel

Mehrere Instanzen von SubChartReporter analysieren den Mehrwährungshandel

Die generierten Objekte werden mit Beschreibungen versehen, die Details aus dem Bericht enthalten (Geschäftszahlen, Volumen, Gewinn und Kommentare). Um die Details anzuzeigen, aktivieren Sie "Objektbeschreibungen anzeigen" in den Chart-Einstellungen.

Wenn die Anzahl der Arbeitssymbole groß ist, werden die Subfenstergrößen verringert. Obwohl dies ein Gesamtbild liefert, kann das Studium von Details schwierig sein. Wenn Sie jede Transaktion analysieren müssen, verwenden Sie so viel Platz wie möglich, einschließlich des Hauptfensters. Zu diesem Zweck erstellen wir eine neue Version des Indikators SubChartReporter, der die Deals auf dem Hauptchart anzeigt, anstatt das Subfenster zu verwenden. Nennen wir ihn MainChartReporter.

Der Indikator MainChartReporter

Da der Indikator auf dem Preischart angezeigt wird, ist es nicht erforderlich, Puffer zu berechnen und andere als die Trendobjekte zu zeichnen. Mit anderen Worten, dies ist ein pufferloser Indikator, der das Arbeitssymbol des aktuellen Diagramms ändert und das analysierte Symbol setzt. Aus Sicht der Implementierung ist der Indikator fast fertig: Der neue Versionscode ist ein deutlich vereinfachter SubChartReporter. Hier sind die Code-Merkmale.

Der Quellcode der drei Hauptklassen Prozessor, ReportProzessor und HistoryProzessor wird in die Header-Datei verschoben und in beide Indikatoren aufgenommen. Die Unterschiede, die für jede der Versionen spezifisch sind, werden in Präprozessoranweisungen für die bedingte Kompilierung verwendet. Das Makro CHART_REPORTER_SUB wird für den Indikatorcode des SubChartReporter definiert, und CHART_REPORTER_MAIN wird für den MainChartReporter definiert.

Dem MainChartReporter müssen zwei Zeilen hinzugefügt werden. Wir müssen das Chart auf ein neues reales Symbol in der Methode 'apply' umstellen:

#ifdef CHART_REPORTER_MAIN
      ChartSetSymbolPeriod(0, real, _Period);
#endif

Wir müssen auch einen Kommentar mit dem Text anzeigen, der in der ersten Indikatorversion gleich seinem Namen war (INDICATOR_SHORTNAME).

#ifdef CHART_REPORTER_MAIN
      Comment(title);
#endif

In der Funktion onChartEvent haben wir das aktuelle Chart aktualisiert, da die erste Indikatorversion Daten über das Symbol lieferte, die sich vom Symbol des Hauptfensters unterschieden. In der neuen Version werden die Kurse der Hauptsymbole "wie besehen" verwendet, so dass eine Aktualisierung des Fensters nicht erforderlich ist. Daher wird die entsprechende Zeile nur bei der bedingten Kompilierung des Indikators SubChartReporter hinzugefügt.

#ifdef CHART_REPORTER_SUB
            ChartSetSymbolPeriod(0, _Symbol, _Period);
#endif

Löschen Sie die Puffer aus dem Quellcode des MainChartReporter-Indikators (indem Sie ihre Nummer gleich 0 angeben, um Compiler-Warnungen zu vermeiden).

#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots   0

Löschen Sie auch die Gruppe der Einstellungen der Subfenster, die nicht länger benötigt werden:

input GroupSettings Chart_Settings; // E I N S T E L L U N G E N des U N T E R F E N S T E R S
input string SubSymbol = ""; // · Symbol
input bool Exact = true; // · Genau

Die Funktion OnCalculate wird leer, muss aber im Indikator vorhanden sein. Der Timer für den Empfang des Berichts von Daten wird in OnInit gestartet.

void OnTimer()
{
  EventKillTimer();
  
  if(processor.isEmpty()) // laden der Dateien nur einmal
  {
    if(processor.attach(ReportFile))
    {
      processor.apply();
      datetime start = processor.getStart();
      if(start != 0)
      {
        ChartSetInteger(ChartID(), CHART_AUTOSCROLL, false);
        // FIXME: das arbeitet nicht wie geplant
        ChartNavigate(ChartID(), CHART_END, -1 * (iBarShift(_Symbol, _Period, start)));
      }
    }
  }
}

Dies war ein Versuch, das Chart zum ersten Deal zu scrollen, indem Sie ChartNavigate aufrufen. Leider konnte ich micht erreichen, dass dieser Codeteil richtig funktioniert, so dass das Chart nie verschoben wurde. Die mögliche Lösung besteht darin, die aktuelle Position zu ermitteln und mit CHART_CURRENT_POS relativ dazu zu navigieren. Diese Lösung erscheint jedoch nicht optimal.

So sieht der Indikator MainChartReporter auf dem Chart aus.

Der Indikator MainChartReporter

Der Indikator MainChartReporter

Angehängte Dateien

Schlussfolgerung

Wir haben mehrere Indikatoren betrachtet, die die Kurse und Handelsgeschäfte mehrerer Symbole visualisieren. Als Quelle für die Eingabedaten dienen Berichte im HTML-Format. Durch die Verwendung eines Universalparsers ist es möglich, nicht nur Standardberichte (Einstellungen, die bereits im Quellcode enthalten sind), sondern auch andere Berichtstypen zu analysieren. Außerdem haben wir die Unterstützung für das CSV-Format implementiert, in dem normalerweise die Handelshistorie der mql5.com-Signale bereitgestellt wird. Durch die Nutzung der Open Source kann jeder die Anwendung an seine Bedürfnisse anpassen.