Die Analyse der Handelsergebnisse mit den HTML-Berichten

Dmitry Fedoseev | 22 Februar, 2019

Einführung

Wenn ein Händler versucht, für Investoren interessant zu sein, ist es sehr wahrscheinlich, dass er seine Handelsergebnisse überprüfen möchte. Ein Händler sollte also in der Lage sein, die Handelshistorie darzustellen, um die Ergebnisse zu demonstrieren. Der MetaTrader 5 ermöglicht das Speichern des Handelshistorie in einer Datei (Toolbox - Registerkarte Trading - Kontextmenü - Bericht). Ein Bericht kann als XLSX (zur Analyse in Microsoft Excel) und als HTML-Datei gespeichert werden, die in jedem Browser angezeigt werden kann. Die zweite Option ist offensichtlich verbreiteter, da einige Nutzer möglicherweise kein Excel haben, während jeder einen Browser hat. Daher wäre ein HTML-Bericht mit den Handelsergebnissen für einen potenziellen Investor besser geeignet.

Zusätzlich zu den Standardmetriken, die in solchen Berichten verfügbar sind, können Sie Ihre eigenen Werte aus Handelsdaten berechnen, die aus einem Bericht extrahiert werden können. In diesem Artikel werden wir die Methoden zur Extraktion von Daten aus HTML-Berichten betrachten. Zuerst werden wir die Struktur der Berichte analysieren und dann eine Funktion schreiben, um sie zu analysieren. Der Name des Berichts wird an die Funktion übergeben. Anschließend gibt die Funktion eine Struktur mit den relevanten Daten zurück. Diese Struktur ermöglicht den direkten Zugriff auf jeden beliebigen Berichtswert. Mit dieser Struktur können Sie einfach und schnell eigene Berichte mit beliebigen Kennzahlen erstellen.

Zusätzlich zu den Handelsberichten ermöglicht MetaTrader 5 das Speichern von Test- und Optimierungsberichten des Expert Advisors. Ähnlich wie bei der Handelshistorie kann ein Prüfbericht in zwei Formaten gespeichert werden: XLSX und HTML, während der Optimierungsbericht in XML gespeichert wird.

In diesem Artikel beschäftigen wir uns mit dem HTML-Testbericht, dem XML-Optimierungsbericht und dem HTML-Bericht über die Handelshistorie.

HTML- und XML-Dateien

Eine HTML-Datei ist eigentlich eine Textdatei, die aus Text (angezeigte Daten) und Tags besteht, die angeben, wie die Daten angezeigt werden sollen (Abb. 1). Jedes Tag beginnt mit dem Zeichen "<" und endet mit ">". Das Tag <br> bedeutet beispielsweise, dass der darauf folgende Text in einer neuen Zeile beginnen soll, während <p> bedeutet, dass der Text ein neuer Absatz ist (nicht nur in einer neuen Zeile, sondern auch nach einer Leerzeile). Zusätzliche Attribute können innerhalb von Tags platziert werden, z.B. <p color="red"> bedeutet, dass der Text nach dem Tag mit einem neuen Absatz beginnen und rot geschrieben werden sollte. 

Eine HTML-Datei im Notepad
Abb. 1. Ein Ausschnitt aus einer HTML-Datei, geöffnet im Notepad

Um die Tag-Aktion zu beenden, wird ein schließender Tag verwendet, der dem öffnenden ähnlich ist aber zusätzlich einen Schrägstrich "/" am Anfang hat. Beispielsweise ist </p> das abschließende Tag für einen Absatz. Einige Tags werden ohne schließende Gegenstücke verwendet, wie z.B. <br>. Einige Tags können optional mit einem abschließenden Tag verwendet werden. Ein neuer Absatz kann gestartet werden, ohne den vorherigen zu schließen. Wenn jedoch ein Farbattribut innerhalb des Tags verwendet wird, wie im obigen Beispiel mit <p>, muss ein schließender Tag verwendet werden, um die weitere Einfärbung des Textes zu verhindern. Es gibt Tags, für die ein Schließen erforderlich sind, wie z.B. <table>. Eine Tabelle sollte immer mit einem abschließenden Tag </table> enden. Die Tabelle besteht aus Zeilen, die durch die eröffnende <tr> und schließende Tags </tr> gekennzeichnet sind. Zellen innerhalb der Zeilen werden durch <td> und </td> definiert. Manchmal werden Zellen mit <th> (Kopfzelle) gekennzeichnet. Ein und dieselbe Tabelle kann beide Zellen mit <td> und diejenigen mit <th> versehen haben. Schließende Tags sin für Zeilen und Zellen erforderlich.

Derzeit werden die meisten HTML-Attribute praktisch nicht verwendet. Stattdessen wird ein "style"-Attribut verwendet, in dem das Aussehen des Elements beschrieben wird. Zum Beispiel: <p style="color: red"> bezeichnet einen Absatz in rot, ist aber auch keine sehr beliebte Methode. Am gebräuchlichsten ist die Verwendung des Attributs "class", in dem nur der Name der Style-Klasse angegeben ist, wie z.B. <p class="small">, während sich die Klasse selbst (Style Description) am Anfang des HTML-Dokuments oder in einer separaten CSS-Datei (Cascading Style Sheet) befindet. 

XML-Dateien (Abb. 2) sind dem HTML sehr ähnlich. Der Hauptunterschied besteht darin, dass HTML einen streng begrenzte Anzahl von Tags unterstützt, während XML die Erweiterung der Tags und das Hinzufügen von benutzerdefinierten Tags ermöglicht.

XML-Datei im Notepad
Abb. 2. Der Ausschnitt einer XML-Datei, die in Notepad geöffnet wurde.

Der Hauptzweck von HTML ist die Datenanzeige, deshalb hat es einen Standardsatz von Tags. Dadurch haben HTML-Dokumente in verschiedenen Browsern nahezu das gleiche Erscheinungsbild. Der Hauptzweck von XML ist das Speichern und Weitergeben von Daten, daher ist die Möglichkeit einer beliebigen Datenstrukturierung wichtig, während der Nutzer, der mit dieser Datei arbeitet, den Zweck und die zu extrahierenden Daten verstehen muss.

Zeichenkettenfunktionen oder Reguläre Ausdrücke (RegEx)

Normalerweise werden mit "Regulären Ausdrücken" (kurz RegEx) Daten aus einem Text extrahiert. In der CodeBase, gibt es die Bibliothek RegularExpressions, um mit Regulären Ausdrücken zu arbeiten. Sie können sich auch im Artikel "Reguläre Ausdrücke für Trader" über die Verwendungsweise erkundigen. Natürlich sind Reguläre Ausdrücke ein sehr mächtiges und komfortables Werkzeug zum Parsen von Textdaten. Wenn Sie sich oft mit dem Parsen von Daten beschäftigen müssen, während Sie verschiedene Aufgaben ausführen, erfordert dies definitiv die Verwendung von Regulären Ausdrücken.

Es gibt jedoch ein paar Nachteile der Regulären Ausdrücke: Sie müssen die Ausdrücke intensiv studieren, bevor Sie sie verwenden können. Die gesamte Idee, die sich auf Reguläre Ausdrücke bezieht, ist recht umfangreich. Man kann sie nicht nur verwenden, indem man "die Referenz bei Bedarf überprüft". Man muss die Theorie gründlich studieren und praktische Fähigkeiten erwerben. Ich denke, der Ansatz, der für die Verwendung von Regulären Ausdrücken erforderlich ist, unterscheidet sich stark von dem, der für die Lösung gewöhnlicher Programmieraufgaben erforderlich ist. Auch wenn man die Möglichkeit hat, Reguläre Ausdrücke zu verwenden, kann es zu Schwierigkeiten beim Wechsel zur Arbeit mit Regulären Ausdrücken und zurück kommen. 

Wenn das Parsen von Daten nicht schwierig und nicht häufig ist, können man die standardmäßigen String-Funktionen verwenden. Darüber hinaus gibt es die Meinung, dass Zeichenkettenfunktionen schneller sind. Sehr wahrscheinlich hängt die Geschwindigkeit von der Art der Aufgabe und den Bedingungen der Anwendung von Regulären Ausdrücken ab. Zeichenkettenfunktionen bieten jedoch eine gute Lösung für die Aufgabe des Parsens von Daten.

Von allen Zeichenkettenfunktionen, die in der Sprachreferenz zur Verfügung stehen, werden wir nur wenige benötigen: StringFind(), StringSubstr(), StringReplace(). Wir benötigen möglicherweise zusätzlich einige sehr einfache Funktionen, wie StringLen(), StringTrimLeft(), StringTrimRight().

Bericht des Testers

Der Bericht des Testers ist der größte, also beginnen wir damit. Dies wird es uns ermöglichen, die Aufgabe zu verstehen und mit allen auftretenden Schwierigkeiten umzugehen. Bei der Erstellung dieses Artikels habe ich den ExpertMACD EA-Prüfbericht mit Standardparametern verwendet (der Bericht ist unten angehängt). Wir öffnen den Bericht im Browser, um zu sehen, womit wir es zu tun haben (Abb. 3).

HTML-Berichtsabschnitte
Abb. 3. Testbericht in HTML, im Browser geöffnet. Rote Linien mit blauem Text zeigen die wichtigsten Berichtsabschnitte an.

Die Berichtsdaten sind in mehrere Abschnitte unterteilt: Berichtsname, Parameter, Ergebnisse, Diagramme, Orders Deals, Summe.  

Jeder Browser hat einen Befehl zum Anzeigen des Seitencodes: Klicken Sie mit der rechten Maustaste auf eine Seite, um das Kontextmenü zu öffnen, und wählen Sie "Seitenquelle anzeigen" oder einen ähnlichen Befehl. 

Beim Betrachten des Quellcodes können wir feststellen, dass alle Berichtsdaten in Tabellen (das Tag <table>) angeordnet sind. Der Testerbericht besteht aus zwei Tabellen. Die erste Tabelle enthält allgemeine Daten: vom Namen des Berichts bis zum Wert "Average consecutive losses", d.h. bis zum Abschnitt Orders. Die zweite Tabelle enthält Daten über Aufträge, Geschäfte und den endgültigen Kontostand. Daten unterschiedlicher Art werden innerhalb einer Tabelle mit dem Attribut "colspan", das mehrere Zellen verbindet, platziert. Die Namen der gemeinsamen Parameter und ihre Werte befinden sich in verschiedenen Zellen der Tabelle, manchmal erscheinen diese Zellen in derselben Zeile, manchmal befinden sie sich in verschiedenen Zeilen.

Ein wichtiger Moment ist die Verfügbarkeit von "id"-Attributen, die eindeutige Identifikatoren von Tags sind. Die id-Werte können verwendet werden, um Zellen mit benötigten Daten zu finden. Die Attribute "id" werden jedoch nicht verwendet. Deshalb werden wir die Parameter finden, indem wir Zeilen und Zellen zählen. So steht beispielsweise der Name "Strategy Tester Report" in der ersten Tabelle, ersten Zeile und ersten Zelle. 

Die Anzahl der Aufträge und Geschäfte in der zweiten Tabelle wird in allen Berichten unterschiedlich sein, so dass wir das Ende der Aufträge und den Beginn der Geschäfte finden müssen. Den Deals folgt eine Zeile mit einer Zelle, dann kommt eine Zeile mit Überschriften - das ist ein Hinweis auf die Trennung von Daten. Auf die Anzahl der Zeilen und Zellen wird nun nicht näher eingegangen. Ein bequemer Weg wird später vorgestellt.

Alle Daten befinden sich in Tabellenzellen, daher müssen wir im Anfangsstadium nur Tabellen, Zeilen und Zellen extrahieren.

Lassen Sie uns die Handelsgeschichte und die Optimierungsberichte analysieren. Der Bericht der Historie ist dem Prüfbericht sehr ähnlich, mit Ausnahme des zusätzlichen Abschnitts mit den Kontosalden (Abb. 4).

Bericht über die Handelshistorie in HTML, geöffnet im Browser.

Abb. 4. Bericht über die Handelshistorie in HTML, geöffnet im Browser. Rote Linien mit blauem Text zeigen die wichtigsten Berichtsabschnitte an.

Durch die Untersuchung des HTML-Codes der Handelshistorie können wir sehen, dass Zelle nicht nur durch <td> Tags definiert sind, sondern auch durch <th>, das für eine Zelle mit einer Überschrift steht. Zusätzlich befinden sich alle Daten in einer Tabelle.

Der Optimierungsbericht ist sehr einfach — er hat einen Datenbereich mit einer Tabelle. Aus dem Quellcode ersehen wir, dass er mit dem Tag <Table>, die Zeilen mit <Row> und die Zellen mit <Cell> erstellt wurde (alle Tags werden neben entsprechenden abschließenden Tags verwendet).

Laden der Datei

Bevor wir irgendwelche Operationen mit den Berichtsdaten durchführen können, müssen wir sie aus der Datei lesen. Dazu verwenden wir die folgende Funktion, die den gesamten Dateiinhalt in einer Variablen von Typ Zeichenkette zurückgibt:

bool FileGetContent(string aFileName,string & aContent){
   int h=FileOpen(aFileName,FILE_READ|FILE_TXT);
   if(h==-1)return(false);
   aContent="";
   while(!FileIsEnding(h)){
      aContent=aContent+FileReadString(h);
   }
   FileClose(h);
   return(true);
}

Der erste Parameter, der der Funktion übergeben wird, ist der Name der Berichtsdatei. Der zweite Parameter ist die Variable, die den Inhalt der Datei enthält. Die Funktion gibt je nach Ergebnis true/false zurück.

Extrahieren der Tabellen

Lassen Sie uns zwei Strukturen verwenden, um Tabellendaten zu finden. Die Struktur, die eine Tabellenzeile enthält:

struct Str{
   string td[];
};

Jedes Element des Arrays td[] wird den Inhalt einer Zelle beinhalten.

Die Struktur enthält die gesamte Tabelle (alle Zeilen):

struct STable{
   Str tr[];
};

Die Daten werden wie folgt aus dem Bericht extrahiert: Zuerst finden wir die Tabelle, die mit dem öffnenden Tag beginnt. Da Tags Attribute haben können, werden wir nur nach dem Anfang des Tags suchen: "<table". Nachdem wir den Anfang des öffnenden Tags gefunden haben, finden wir sein Ende, ">". Dann suchen wir nach dem Tabellenende, d.h. dem abschließenden Tag "</table>". Dies ist einfach, da Berichte keine verschachtelten Tabellen haben, d.h. jedes öffnende Tag wird von einem abschließenden Tag gefolgt.

Nachdem wir die Tabelle gefunden haben, wiederholen wir dasselbe für Zeilen mit "<tr" für ihren Anfang und "</tr" für ihr Ende. Dann finden wir in jeder Zeile den Anfang oder die Zellen mit "<td" und ihr Ende mit </td>. Die Aufgabe ist bei Zeilen etwas komplizierter, da eine Zelle sowohl das Tag <td> als auch <th> enthalten können. Aus diesem Grund werden wir anstelle von StringFind() die benutzerdefinierte Funktion TagFind() verwenden:

int TagFind(string aContent,string & aTags[],int aStart,string & aTag){
   int rp=-1;
   for(int i=0;i<ArraySize(aTags);i++){
      int p=StringFind(aContent,"<"+aTags[i],aStart);
      if(p!=-1){
         if(rp==-1){
            rp=p;
            aTag=aTags[i];
         }
         else{
            if(p<rp){
               rp=p;
               aTag=aTags[i];
            }
         }      
      }
   }
   return(rp);
}

Die Parameter der Funktion:

  • string aContent — die Zeichenkette, in der wir suchen;
  • string & aTags[] — ein Array mit Tags;
  • int aStart — die Position, an der die Suche gestartet werden soll;
  • string & aTag — das gefundene Tag wird als Referenz zurückgegeben.

Im Gegensatz zu StringFind() wird ein Array der Funktion TagFind() und nicht die gesuchte Zeichenkette übergeben. Das öffnende "<" wird den Suchbegriffen in der Funktion hinzugefügt. Die Funktion gibt die Position des nächstgelegenen Tags zurück.

Mit TagFind() werden wir systematisch nach öffnenden und schließenden Tags suchen. Alles, was zwischen ihnen gefunden wird, wird zu einem Array zusammengefasst:

int TagsToArray(string aContent,string & aTags[],string & aArray[]){
   ArrayResize(aArray,0);
   int e,s=0;
   string tag;
   while((s=TagFind(aContent,aTags,s,tag))!=-1 && !IsStopped()){  
      s=StringFind(aContent,">",s);
      if(s==-1)break;
      s++; 
      e=StringFind(aContent,"</"+tag,s);   
      if(e==-1)break;  
      ArrayResize(aArray,ArraySize(aArray)+1);
      aArray[ArraySize(aArray)-1]=StringSubstr(aContent,s,e-s);  
   }
   return(ArraySize(aArray));
}

Die Parameter der Funktion:

  • string aContent — die Zeichenkette, in der wir suchen;
  • string & aTags[] — ein Array mit Tags;
  • string aArray[] — ein Array mit dem Inhalt aller gefundenen Tags wird per Referenz zurückgegeben.

Ein Array der gesuchten Tags wird nur beim Parsen von Zellen an TagsToArray() übergeben. Deshalb werden wir in allen anderen Fällen ein Analogon der Funktion mit einem üblichen Zeichenkettenparameter schreiben:

int TagsToArray(string aContent,string aTag,string & aArray[]){
   string Tags[1];
   Tags[0]=aTag;
   return(TagsToArray(aContent,Tags,aArray));
}

Die Parameter der Funktion:

  • string aContent — die Zeichenkette, in der wir suchen;
  • string & aTag — gesuchtes Taq;
  • string aArray[] — ein Array mit dem Inhalt aller gefundenen Tags wird per Referenz zurückgegeben.

Kommen wir nun zu der Funktion, die den Tabelleninhalt extrahieren kann. Der Zeichenkettenparameter aFileName mit dem Namen der geparsten Datei wird der Funktion übergeben. Eine lokale Zeichenkettenvariable für den Dateiinhalt und ein lokales Array der Strukturen STable werden in der Funktion verwendet:

STable tables[];
string FileContent;

Der Gesamtinhalt des Berichtes mittel der Funktion FileGetContent:

if(!FileGetContent(aFileName,FileContent)){
   return(true);
}

Hier sind die Hilfsvariablen:

string tags[]={"td","th"};
string ttmp[],trtmp[],tdtmp[];
int tcnt,trcnt,tdcnt;

Mögliche Optionen für die Zell-Tags finden sich im Array 'tags'. Der Inhalt der Tabellen, Zeilen und Zellen wird temporär in den Zeichenketten ttmp[], trtmp[] und tdtmp[] platziert. 

Daten aus der Tabelle abrufen:

tcnt=TagsToArray(FileContent,"table",ttmp);
ArrayResize(tables,tcnt);

Schleife über alle Tabellen, extrahieren der Zeilen und dann der Zellen aus der Zeile:

for(int i=0;i<tcnt;i++){
   trcnt=TagsToArray(ttmp[i],"tr",trtmp);
   ArrayResize(tables[i].tr,trcnt);      
   for(int j=0;j<trcnt;j++){         
      tdcnt=TagsToArray(trtmp[j],tags,tdtmp);
      ArrayResize(tables[i].tr[j].td,tdcnt);
      for(int k=0;k<tdcnt;k++){  
         tables[i].tr[j].td[k]=RemoveTags(tdtmp[k]);
      }
   }
} 

Tabellen, Zeilen und Zellen werden zunächst in temporären Arrays empfangen und dann wird das Array der Zellen einer Struktur zugewiesen. Während des Kopierens wird eine Zelle durch den Befehl RemoveTags() gefiltert. Tabellenzellen können verschachtelte Tags haben. RemoveTags() löscht sie und belässt nur die benötigten Tags.

Die Funktion RemoveTags():

string RemoveTags(string aStr){
   string rstr="";
   int e,s=0;
   while((e=StringFind(aStr,"<",s))!=-1){
      if(e>s){

         rstr=rstr+StringSubstr(aStr,s,e-s);
      }
      s=StringFind(aStr,">",e);
      if(s==-1)break;
      s++;
   }
   if(s!=-1){
      rstr=rstr+StringSubstr(aStr,s,StringLen(aStr)-s);
   }
   StringReplace(rstr,"&nbsp;"," ");
   while(StringReplace(rstr,"  "," ")>0);
   StringTrimLeft(rstr);
   StringTrimRight(rstr);
   return(rstr);
}

Betrachten wir die Funktion RemoveTags(). Der Index "s" wird für den Anfang der verwendeten Daten verwendet. Sein Wert ist zunächst 0, da die Daten vom Zeilenanfang an beginnen können. Die Öffnungsklammer "<", d.h. der Tag-Anfang wird in der "while"-Schleife gesucht. Wenn der Tag-Anfang gefunden wird, werden alle Daten von der in der Variablen "s" angegebenen Position bis zur gefundenen Position der Variable "rstr" zugewiesen. Danach wird das Ende des Tags durchsucht, d.h. der Neuanfang von Nutzdaten. Wenn der Wert des Index' "s" nach der Schleife ungleich -1 ist (d.h. die Zeichenkette endet mit Nutzdaten, wurde aber nicht kopiert), werden die Daten der Variable "rstr" zugewiesen. Am Funktionsende wird das Leerzeichen &nbsp; durch ein einfaches Leerzeichen ersetzt, während wiederholte Leerzeichen sowie Leerzeichen am Anfang und Ende der Zeichenkette gelöscht werden.

In diesem Schritt haben wir das Array "tables" der Strukturen, die mit reinen Tabellendaten gefüllt sind. Speichern wir dieses Array in einer Textdatei. Beim Speichern setzen wir Zahlen für die Tabellen, Zeilen und Zellen (die Daten werden in der Datei 1.txt gespeichert):

int h=FileOpen("1.txt",FILE_TXT|FILE_WRITE);

for(int i=0;i<ArraySize(tables);i++){
   FileWriteString(h,"table "+(string)i+"\n");
   for(int j=0;j<ArraySize(tables[i].tr);j++){      
      FileWriteString(h,"   tr "+(string)j+"\n");         
      for(int k=0;k<ArraySize(tables[i].tr[j].td);k++){
         FileWriteString(h,"      td "+(string)k+": "+tables[i].tr[j].td[k]+"\n");
      }
   }
}

FileClose(h);

Mit dieser Datei können wir leicht erkennen, in welchen Zellen die Daten liegen. Unten ein Ausschnitt aus der Datei:

table 0
   tr 0
      td 0: Strategy Test report
   tr 1
      td 0: IMPACT-Demo (Build 1940)
   tr 2
      td 0: 
   tr 3
      td 0: Settings
   tr 4
      td 0: Expert Advisor:
      td 1: ExpertMACD
   tr 5
      td 0: Symbol:
      td 1: USDCHF
   tr 6
      td 0: Period:
      td 1: H1 (2018.11.01 - 2018.12.01)
   tr 7
      td 0: Parameters:
      td 1: Inp_Expert_Title=ExpertMACD
   tr 8
      td 0: 
      td 1: Inp_Signal_MACD_PeriodFast=12
   tr 9
      td 0: 
      td 1: Inp_Signal_MACD_PeriodSlow=24
   tr 10
      td 0: 
      td 1: Inp_Signal_MACD_PeriodSignal=9
   tr 11

Struktur für die Berichtsdaten

Jetzt kommen wir zu einer etwas langweiligen Routine: Wir müssen eine Struktur schreiben, die den Berichtsdaten entspricht und dieser Struktur die Daten aus dem Tabellen-Array zuweisen. Der Bericht ist in mehrere Abschnitte unterteilt. Daher werden wir mehrere Strukturen verwenden, die zu einer allgemeinen Struktur zusammengefasst sind.

Struktur für den Abschnitt mit den Einstellungen:

struct SSettings{
   string Expert;
   string Symbol;
   string Period;
   string Inputs;
   string Broker;
   string Currency;
   string InitialDeposit;
   string Leverage;
};

Der Zweck der Strukturfelder ergibt sich aus ihren Namen. Alle Felder der Struktur enthalten die Daten genau so, wie sie im Bericht erscheinen. Die Liste der Parameter des Expert Advisor befindet sich in einer Zeichenkettenvariablen "Inputs".

Struktur für den Abschnitt mit den Ergebnisdaten:

struct SResults{
   string HistoryQuality;
   string Bars;
   string Ticks;
   string Symbols;
   string TotalNetProfit;
   string BalanceDrawdownAbsolute;
   string EquityDrawdownAbsolute;
   string GrossProfit;
   string BalanceDrawdownMaximal;
   string EquityDrawdownMaximal;
   string GrossLoss;
   string BalanceDrawdownRelative;
   string EquityDrawdownRelative;
   string ProfitFactor;
   string ExpectedPayoff;
   string MarginLevel;
   string RecoveryFactor;
   string SharpeRatio;
   string ZScore;
   string AHPR;
   string LRCorrelation;
   string OnTesterResult;
   string GHPR;
   string LRStandardError;
   string TotalTrades;
   string ShortTrades_won_pers;
   string LongTrades_won_perc;
   string TotalDeals;
   string ProfitTrades_perc_of_total;
   string LossTrades_perc_of_total;
   string LargestProfitTrade;
   string LargestLossTrade;
   string AverageProfitTrade;
   string AverageLossTrade;
   string MaximumConsecutiveWins_cur;
   string MaximumConsecutiveLosses_cur;
   string MaximalConsecutiveProfit_count;
   string MaximalConsecutiveLoss_count;
   string AverageConsecutiveWins;
   string AverageConsecutiveLosses;
   string Correlation_Profits_MFE;
   string Correlation_Profits_MAE;
   string Correlation_MFE_MAE;      
   string MinimalPositionHoldingTime;
   string MaximalPositionHoldingTime;
   string AveragePositionHoldingTime;
};

Struktur für die Daten eines Auftrags:

struct SOrder{
   string OpenTime;
   string Order;
   string Symbol;
   string Type;
   string Volume;
   string Price;
   string SL;
   string TP;
   string Time;
   string State;
   string Comment;
};

Struktur für die Daten eines Deals:

struct SDeal{
   string Time;
   string Deal;
   string Symbol;
   string Type;
   string Direction;
   string Volume;
   string Price;
   string Order;
   string Commission;
   string Swap;
   string Profit;
   string Balance;
   string Comment;
};

Struktur für den sich ergebenden Saldo

struct SSummary{
   string Commission;
   string Swap;
   string Profit;
   string Balance;
};

Allgemeine Struktur:

struct STestingReport{
   SSettings Settings;
   SResults Results;
   SOrder Orders[];
   SDeal Deals[];
   SSummary Summary;
};

Die Arrays der Strukturen SOrder und SDeals werden für Aufträge und Deals verwendet.

Zuweisen der Daten

Aberr auch dieses Stück Routinearbeit erfordert Aufmerksamkeit. Betrachten wir die zuvor erhaltene Textdatei mit den Tabellendaten und füllen die Struktur aus:

aTestingReport.Settings.Expert=tables[0].tr[4].td[1];
aTestingReport.Settings.Symbol=tables[0].tr[5].td[1];
aTestingReport.Settings.Period=tables[0].tr[6].td[1];
aTestingReport.Settings.Inputs=tables[0].tr[7].td[1];

In der letzten Zeile des obigen Code-Ausschnitts wird dem Feld Inputs ein Wert zugewiesen, danach speichert das Feld jedoch nur noch Daten eines Parameters. Dann werden weitere Parameter gesammelt. Diese Parameter befinden sich ab Zeile 8, während die erste Zelle in jeder Parameterzeile leer ist. Die Schleife wird ausgeführt, solange die erste Zelle in der Zeile leer ist:

int i=8;
while(i<ArraySize(tables[0].tr) && tables[0].tr[i].td[0]==""){
   aTestingReport.Settings.Inputs=aTestingReport.Settings.Inputs+", "+tables[0].tr[i].td[1];
   i++;
}

Unten ist die Funktion für das Parsen des kompletten Testberichts:

bool TestingHTMLReportToStruct(string aFileName,STestingReport & aTestingReport){

   STable tables[];

   string FileContent;
   
   if(!FileGetContent(aFileName,FileContent)){
      return(true);
   }

   string tags[]={"td","th"};
   string ttmp[],trtmp[],tdtmp[];
   int tcnt,trcnt,tdcnt;
   
   tcnt=TagsToArray(FileContent,"table",ttmp);

   ArrayResize(tables,tcnt);
   
   for(int i=0;i<tcnt;i++){
      trcnt=TagsToArray(ttmp[i],"tr",trtmp);
      ArrayResize(tables[i].tr,trcnt);      
      for(int j=0;j<trcnt;j++){         
         tdcnt=TagsToArray(trtmp[j],tags,tdtmp);
         ArrayResize(tables[i].tr[j].td,tdcnt);
         for(int k=0;k<tdcnt;k++){  
            tables[i].tr[j].td[k]=RemoveTags(tdtmp[k]);
         }
      }
   }   
   
   // settings section
   
   aTestingReport.Settings.Expert=tables[0].tr[4].td[1];
   aTestingReport.Settings.Symbol=tables[0].tr[5].td[1];
   aTestingReport.Settings.Period=tables[0].tr[6].td[1];
   aTestingReport.Settings.Inputs=tables[0].tr[7].td[1];
   int i=8;
   while(i<ArraySize(tables[0].tr) && tables[0].tr[i].td[0]==""){
      aTestingReport.Settings.Inputs=aTestingReport.Settings.Inputs+", "+tables[0].tr[i].td[1];
      i++;
   }
   aTestingReport.Settings.Broker=tables[0].tr[i++].td[1];
   aTestingReport.Settings.Currency=tables[0].tr[i++].td[1];  
   aTestingReport.Settings.InitialDeposit=tables[0].tr[i++].td[1];
   aTestingReport.Settings.Leverage=tables[0].tr[i++].td[1];   
   
   // results section
   
   i+=2;
   aTestingReport.Results.HistoryQuality=tables[0].tr[i++].td[1];
   aTestingReport.Results.Bars=tables[0].tr[i].td[1];
   aTestingReport.Results.Ticks=tables[0].tr[i].td[3];
   aTestingReport.Results.Symbols=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.TotalNetProfit=tables[0].tr[i].td[1];
   aTestingReport.Results.BalanceDrawdownAbsolute=tables[0].tr[i].td[3];
   aTestingReport.Results.EquityDrawdownAbsolute=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.GrossProfit=tables[0].tr[i].td[1];
   aTestingReport.Results.BalanceDrawdownMaximal=tables[0].tr[i].td[3];
   aTestingReport.Results.EquityDrawdownMaximal=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.GrossLoss=tables[0].tr[i].td[1];
   aTestingReport.Results.BalanceDrawdownRelative=tables[0].tr[i].td[3];
   aTestingReport.Results.EquityDrawdownRelative=tables[0].tr[i].td[5];
   i+=2;
   aTestingReport.Results.ProfitFactor=tables[0].tr[i].td[1];
   aTestingReport.Results.ExpectedPayoff=tables[0].tr[i].td[3];
   aTestingReport.Results.MarginLevel=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.RecoveryFactor=tables[0].tr[i].td[1];
   aTestingReport.Results.SharpeRatio=tables[0].tr[i].td[3];
   aTestingReport.Results.ZScore=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.AHPR=tables[0].tr[i].td[1];
   aTestingReport.Results.LRCorrelation=tables[0].tr[i].td[3];
   aTestingReport.Results.tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.GHPR=tables[0].tr[i].td[1];
   aTestingReport.Results.LRStandardError=tables[0].tr[i].td[3];
   i+=2;
   aTestingReport.Results.TotalTrades=tables[0].tr[i].td[1];
   aTestingReport.Results.ShortTrades_won_pers=tables[0].tr[i].td[3];
   aTestingReport.Results.LongTrades_won_perc=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.TotalDeals=tables[0].tr[i].td[1];
   aTestingReport.Results.ProfitTrades_perc_of_total=tables[0].tr[i].td[3];
   aTestingReport.Results.LossTrades_perc_of_total=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.LargestProfitTrade=tables[0].tr[i].td[2];
   aTestingReport.Results.LargestLossTrade=tables[0].tr[i].td[4];
   i++;
   aTestingReport.Results.AverageProfitTrade=tables[0].tr[i].td[2];
   aTestingReport.Results.AverageLossTrade=tables[0].tr[i].td[4];
   i++;
   aTestingReport.Results.MaximumConsecutiveWins_cur=tables[0].tr[i].td[2];
   aTestingReport.Results.MaximumConsecutiveLosses_cur=tables[0].tr[i].td[4];
   i++;
   aTestingReport.Results.MaximalConsecutiveProfit_count=tables[0].tr[i].td[2];
   aTestingReport.Results.MaximalConsecutiveLoss_count=tables[0].tr[i].td[4];
   i++;
   aTestingReport.Results.AverageConsecutiveWins=tables[0].tr[i].td[2];
   aTestingReport.Results.AverageConsecutiveLosses=tables[0].tr[i].td[4];    
   i+=6;
   aTestingReport.Results.Correlation_Profits_MFE=tables[0].tr[i].td[1];
   aTestingReport.Results.Correlation_Profits_MAE=tables[0].tr[i].td[3];
   aTestingReport.Results.Correlation_MFE_MAE=tables[0].tr[i].td[5];    
   i+=3;
   aTestingReport.Results.MinimalPositionHoldingTime=tables[0].tr[i].td[1];
   aTestingReport.Results.MaximalPositionHoldingTime=tables[0].tr[i].td[3];
   aTestingReport.Results.AveragePositionHoldingTime=tables[0].tr[i].td[5];   
   
   // orders

   ArrayFree(aTestingReport.Orders);
   int ocnt=0;
   for(i=3;i<ArraySize(tables[1].tr);i++){
      if(ArraySize(tables[1].tr[i].td)==1){
         break;
      }   
      ArrayResize(aTestingReport.Orders,ocnt+1);
      aTestingReport.Orders[ocnt].OpenTime=tables[1].tr[i].td[0];
      aTestingReport.Orders[ocnt].Order=tables[1].tr[i].td[1];
      aTestingReport.Orders[ocnt].Symbol=tables[1].tr[i].td[2];
      aTestingReport.Orders[ocnt].Type=tables[1].tr[i].td[3];
      aTestingReport.Orders[ocnt].Volume=tables[1].tr[i].td[4];
      aTestingReport.Orders[ocnt].Price=tables[1].tr[i].td[5];
      aTestingReport.Orders[ocnt].SL=tables[1].tr[i].td[6];
      aTestingReport.Orders[ocnt].TP=tables[1].tr[i].td[7];
      aTestingReport.Orders[ocnt].Time=tables[1].tr[i].td[8];
      aTestingReport.Orders[ocnt].State=tables[1].tr[i].td[9];
      aTestingReport.Orders[ocnt].Comment=tables[1].tr[i].td[10];      
      ocnt++;
   }
   
   // Deals
   
   i+=3;
   ArrayFree(aTestingReport.Deals);
   int dcnt=0;
   for(;i<ArraySize(tables[1].tr);i++){
      if(ArraySize(tables[1].tr[i].td)!=13){
         if(ArraySize(tables[1].tr[i].td)==6){
            aTestingReport.Summary.Commission=tables[1].tr[i].td[1];
            aTestingReport.Summary.Swap=tables[1].tr[i].td[2];
            aTestingReport.Summary.Profit=tables[1].tr[i].td[3];
            aTestingReport.Summary.Balance=tables[1].tr[i].td[4];            
         }
         break;
      }   
      ArrayResize(aTestingReport.Deals,dcnt+1);   
      aTestingReport.Deals[dcnt].Time=tables[1].tr[i].td[0];
      aTestingReport.Deals[dcnt].Deal=tables[1].tr[i].td[1];
      aTestingReport.Deals[dcnt].Symbol=tables[1].tr[i].td[2];
      aTestingReport.Deals[dcnt].Type=tables[1].tr[i].td[3];
      aTestingReport.Deals[dcnt].Direction=tables[1].tr[i].td[4];
      aTestingReport.Deals[dcnt].Volume=tables[1].tr[i].td[5];
      aTestingReport.Deals[dcnt].Price=tables[1].tr[i].td[6];
      aTestingReport.Deals[dcnt].Order=tables[1].tr[i].td[7];
      aTestingReport.Deals[dcnt].Commission=tables[1].tr[i].td[8];
      aTestingReport.Deals[dcnt].Swap=tables[1].tr[i].td[9];
      aTestingReport.Deals[dcnt].Profit=tables[1].tr[i].td[10];
      aTestingReport.Deals[dcnt].Balance=tables[1].tr[i].td[11];
      aTestingReport.Deals[dcnt].Comment=tables[1].tr[i].td[12];
      dcnt++;
   }
   return(true);
}

Der Dateiname des Berichts wird der Funktion übergeben. Auch die in der Funktion zu füllende Struktur STestingReport wird per Referenz übergeben.

Beachten Sie den Codeausschnitte, die mit "Orders" und "Deals" beginnen. Die Nummer der Zeile mit dem Anfang der Liste der Aufträge ist bereits definiert, während das Ende der Liste, basierend auf einer Zeile, mit einer einzelnen Zelle bestimmt wird:

if(ArraySize(tables[1].tr[i].td)==1){
   break;
}  

Es werden drei Zeilen nach den Aufträgen übergangen:

// Deals
   
i+=3;

Danach werden Daten über die Deals gesammelt. Das Ende der Liste der Deals wird durch die Zeile mit 6 Zellen bestimmt - diese Zeile enthält Daten über den endgültigen Status des Saldos. Vor dem Verlassen der Schleife wird die Struktur der Entwicklung des Saldos gefüllt:

if(ArraySize(tables[1].tr[i].td)!=13){
   if(ArraySize(tables[1].tr[i].td)==6){
      aTestingReport.Summary.Commission=tables[1].tr[i].td[1];
      aTestingReport.Summary.Swap=tables[1].tr[i].td[2];
      aTestingReport.Summary.Profit=tables[1].tr[i].td[3];
      aTestingReport.Summary.Balance=tables[1].tr[i].td[4];            
   }
   break;
} 

Die Struktur mit den Berichtsdaten ist vollständig fertig.

Bericht über den Handelsverlauf

Der Bericht über die Handelshistorie kann ähnlich wie der Bericht des Strategy Tester analysiert werden, obwohl die endgültige Datenstruktur und die darin enthaltenen Strukturen unterschiedlich sein werden:

struct SHistory{
   SHistoryInfo Info;
   SOrder Orders[];
   SDeal Deals[];
   SSummary Summary;  
   SDeposit Deposit;
   SHistoryResults Results;
};

Die Struktur SHistory enthält die folgenden Strukturen: SHistoryInfo — allgemeine Kontoinformationen, Arrays mit den Strukturen der Orders und Deals, SSummary — Handelsergebnisse, SDeposit — Endbestand, SHistoryResults — allgemeine Werte (Gewinn, Anzahl der Geschäfte, Drawdown, etc.).

Der vollständige Funktionscode von HistoryHTMLReportToStruct() zum Parsen von Handelsbericht ist unten angehängt. Der Funktion werden zwei Parameter übergeben:

  • string FileName — der Name der Berichtsdatei
  • SHistory History — die Struktur mit den zuzuweisenden Daten des Handelsberichts

Optimierungsbericht

Der erste Unterschied bezüglich des Optimierungsberichts ist der unterschiedliche Dateityp. Der Bericht ist in ANSI gespeichert, so dass wir eine andere Funktion verwenden werden, um seinen Inhalt zu lesen:

bool FileGetContentAnsi(string aFileName,string & aContent){
   int h=FileOpen(aFileName,FILE_READ|FILE_TXT|FILE_ANSI);
   if(h==-1)return(false);
   aContent="";
   while(!FileIsEnding(h)){
      aContent=aContent+FileReadString(h);
   }
   FileClose(h);
   return(true);
}

Einen weiteren Unterschied weisen die Tags auf. Statt <table>, <tr> und <td> werden folgende Tags verwendet: <Table>, <Row> and <Cell>. Der letzte, wichticgste Unterschied betrifft die Datenstruktur:

struct SOptimization{
   string ParameterName[];
   SPass Pass[];
};

Die Struktur umfasst einen Zeichenkettenarray mit dem Namen der zu optimierenden Parameter (die Spalte ganz rechts) und den Array der Strukturen von SPass:

struct SPass{
   string Pass;
   string Result;
   string Profit;
   string ExpectedPayoff;
   string ProfitFactor;
   string RecoveryFactor;
   string SharpeRatio;
   string Custom;
   string EquityDD_perc;
   string Trades;
   string Parameters[];
};

Die Struktur beinhaltet die Felder, die in den Berichtsspalten enthalten sind. Das letzte Feld ist das Zeichenketten-Array, das die Werte der zu optimierenden Parameter gemäß ihren Namen im Array ParameterName() enthält.

Der vollständige Code der Funktion OptimizerXMLReportToStruct() zum Parsen von Optimierungsberichten ist unten angehängt. Der Funktion werden zwei Parameter übergeben:

  • string FileName — der Name der Berichtsdatei
  • SOptimization Optimization — die Struktur mit den zu füllenden Optimierungsberichtsdaten

Zusatzfunktionen

Wenn wir alle Strukturen und Funktionen, die innerhalb dieses Artikels erstellt wurden, in einer Include-Datei zusammenfassen, dann wird das Parsen des Berichts in drei Zeilen Code implementiert. 

1. Einbinden der Datei:

#include <HTMLReport.mqh>

2. Deklarieren der Struktur:

SHistory History;

3. Aufruf der Funktion:

HistoryHTMLReportToStruct("ReportHistory-555849.html",History);

Danach werden alle Berichtsdaten in den entsprechenden Strukturfeldern abgelegt, genau wie sie im Bericht angeordnet sind. Obwohl alle Felder vom Typ Zeichenkette sind, können sie leicht für Berechnungen verwendet werden. Zu diesem Zweck müssen wir nur die Zeichenkette in eine Zahl umwandeln. Einige der Berichtsfelder und die entsprechenden Strukturfelder enthalten jedoch zwei Werte. Beispielsweise wird das Auftragsvolumen durch zwei Werte "0.1 / 0.1" geschrieben, wobei der erste Wert das Auftragsvolumen und der zweite Wert das gefüllte Volumen ist. Einige Summen haben auch doppelte Werte, der Hauptwert und ein zusätzlicher Wert in Klammern. So kann beispielsweise die Positionsanzahl als "11 (54,55%)" geschrieben werden — die tatsächliche Nummer und ihr relativer Prozentwert. Lassen Sie uns daher Hilfsfunktionen schreiben, um die Verwendung solcher Werte zu vereinfachen.

Funktionen zur Extraktion einzelner Volumenwerte:

string Volume1(string aStr){
   int p=StringFind(aStr,"/",0);
   if(p!=-1){
      aStr=StringSubstr(aStr,0,p);
   }
   StringTrimLeft(aStr);
   StringTrimRight(aStr);
   return(aStr);
}

string Volume2(string aStr){
   int p=StringFind(aStr,"/",0);
   if(p!=-1){
      aStr=StringSubstr(aStr,p+1);
   }
   StringTrimLeft(aStr);
   StringTrimRight(aStr);
   return(aStr);
}

Die Funktion Volume1() ruft den ersten Volumenwert aus einer Zeichenkette wie "0.1 / 0.1" ab, Volume2() ruft den zweiten der Werte ab.

Funktionen zur Extraktion von Werten mit doppelten Daten:

string Value1(string aStr){
   int p=StringFind(aStr,"(",0);
   if(p!=-1){
      aStr=StringSubstr(aStr,0,p);
   }
   StringTrimLeft(aStr);
   StringTrimRight(aStr);
   return(aStr);
}

string Value2(string aStr){
   int p=StringFind(aStr,"(",0);
   if(p!=-1){
      aStr=StringSubstr(aStr,p+1);
   }
   StringReplace(aStr,")","");
   StringReplace(aStr,"%","");
   StringTrimLeft(aStr);
   StringTrimRight(aStr);
   return(aStr);
}

Die Funktion Value1() ruft den ersten Wert aus einer Zeichenkette vom Typ "8.02 (0.08%)" ab, Value2() ruft den zweiten Wert ab, löscht die schließende Klammer und das Prozentzeichen, falls vorhanden.

Eine weitere nützliche Funktion, die benötigt werden kann, ist die zur Konvertierung von Daten über Parameter aus der Struktur des Testberichts. Wir werden die folgende Struktur verwenden, und sie in eine komfortable Form zu überführen:

struct SInput{
   string Name,
   string Value;
}

Der Name des Feldes wird für den Parameternamen verwendet, Value für den Wert. Der Typ des Wertes ist im Voraus bekannt, daher ist das Feld Value vom Typ Zeichenkette.

Die folgende Funktion wandelt eine Zeichenkette mit Parametern in ein Array SInputs von Strukturen um:

void InputsToStruct(string aStr,SInput & aInputs[]){
   string tmp[];
   string tmp2[];
   StringSplit(aStr,',',tmp);
   int sz=ArraySize(tmp);
   ArrayResize(aInputs,sz);
   for(int i=0;i<sz;i++){
      StringSplit(tmp[i],'=',tmp2);
      StringTrimLeft(tmp2[0]);
      StringTrimRight(tmp2[0]);      
      aInputs[i].Name=tmp2[0];
      if(ArraySize(tmp2)>1){
         StringTrimLeft(tmp2[1]);
         StringTrimRight(tmp2[1]);       
         aInputs[i].Value=tmp2[1];
      }
      else{
         aInputs[i].Value="";
      }
   }
}

Die Zeichenkette mit Parametern wird in ein Array nach dem Trennzeichen "," unterteilt. Dann wird jedes Element des resultierenden Arrays in den Namen und den Wert basierend nach dem Trennzeichen "=" unterteilt.

Jetzt ist alles bereit für die Analyse der extrahierten Daten und die Erstellung eigener Berichte.

Erstellen eines nutzerdefinierten Berichts

Da wir nun Zugriff auf die Berichtsdaten haben, können wir diese auf beliebige Weise analysieren. Es gibt viele Möglichkeiten. Ausgehend von den Handelsergebnissen können wir allgemeine Kennzahlen wie die Sharpe-Ratio, den Erholungsfaktor usw. berechnen. Benutzerdefinierte HTML-Berichte ermöglichen den Zugriff auf alle HTML-, CSS- und JavaScript-Funktionen. Mit HTML können wir unsere Berichte neu anordnen. So können wir beispielsweise eine Tabelle mit Aufträgen und Geschäften oder separate Tabellen für Kauf- und Verkaufsgeschäfte und andere Arten von Berichten erstellen.

CSS ermöglicht die Einfärbung der Daten, was die Analyse des Berichts erleichtert. JavaScript kann dabei helfen, die Berichte interaktiv zu gestalten. Wenn Sie beispielsweise mit der Maus über eine Geschäftszeile fahren, kann die entsprechende Auftragszeile in der Auftragstabelle hervorgehoben werden. Bilder können generiert und dem HTML-Bericht hinzugefügt werden. Optional erlaubt JavaScript das Zeichnen von Bildern direkt innerhalb des Canvas-Elements in html5. Es hängt alles von deinen Bedürfnissen, Ihrer Vorstellungskraft und Ihren Fähigkeiten ab.

Lassen Sie uns einen benutzerdefinierten HTML-Handelsbericht erstellen. Lassen Sie uns Aufträge und Deals in einer Tabelle zusammenfassen. Gelöschte Aufträge werden grau dargestellt, da sie keine interessanten Informationen enthalten. Auch grau wird für Marktorders verwendet, da sie die gleichen Informationen liefern wie die aufgrund der Aufträge ausgeführten Deals. Deals werden in hellen Farben hervorgehoben: blau und rot. Ausgefüllte Pending-Orders sollten ebenfalls auffällige Farben haben, wie z.B. rosa und blau. 

Die benutzerdefinierte Berichtstabelle enthält die Überschriften aus den Tabellen der Aufträge und der Deals. Lassen Sie uns geeignete Arrays vorbereiten:

   string TableHeader[]={  "Time",
                           "Order",
                           "Deal",
                           "Symbol",
                           "Type",
                           "Direction",
                           "Volume",
                           "Price",
                           "Order",
                           "S/L",
                           "T/P",
                           "Time",
                           "State",
                           "Commission",
                           "Swap",
                           "Profit",
                           "Balance",
                           "Comment"};

Wir erhalten eine Struktur mit den Berichtsdaten

SHistory History;
HistoryHTMLReportToStruct("ReportHistory-555849.html",History);

Wir öffnen die Datei des erstellten Berichts und schreiben den standardmäßigen Anfang einer HTML-Seite mit der Funktion HTMLStart():

   int h=FileOpen("Report.htm",FILE_WRITE);
   if(h==-1)return;

   FileWriteString(h,HTMLStart("Report"));
   FileWriteString(h,"<table>\n");
   
   FileWriteString(h,"\t<tr>\n");   
   for(int i=0;i<ArraySize(TableHeader);i++){
      FileWriteString(h,"\t\t<th>"+TableHeader[i]+"</th>\n"); 
   }
   FileWriteString(h,"\t</tr>\n");     

The HTMLStart() function code is shown below:

string HTMLStart(string aTitle,string aCSSFile="style.css"){
   string str="<!DOCTYPE html>\n";
   str=str+"<html>\n";
   str=str+"<head>\n";
   str=str+"<link href=\""+aCSSFile+"\" rel=\"stylesheet\" type=\"text/css\">\n";
   str=str+"<title>"+aTitle+"</title>\n";
   str=str+"</head>\n";  
   str=str+"<body>\n";     
   return str;
}

Eine Zeichenkette mit dem Titel der Seite für das Tag <title> und dem Dateinamen mit den Stilen, die der Funktion übergeben wurden

Wir iterieren über alle Aufträge, bestimmen den Typ der Darstellung und formen ihn:

int j=0;
for(int i=0;i<ArraySize(History.Orders);i++){
   
   string sc="";
   
   if(History.Orders[i].State=="canceled"){
      sc="PendingCancelled";
   }
   else if(History.Orders[i].State=="filled"){
      if(History.Orders[i].Type=="buy"){
         sc="OrderMarketBuy";
      }
      else if(History.Orders[i].Type=="sell"){
         sc="OrderMarketSell";
      }
      if(History.Orders[i].Type=="buy limit" || History.Orders[i].Type=="buy stop"){
         sc="OrderPendingBuy";
      }
      else if(History.Orders[i].Type=="sell limit" || History.Orders[i].Type=="sell stop"){
         sc="OrderMarketSell";
      }         
   }

   FileWriteString(h,"\t<tr class=\""+sc+"\">\n");   
   FileWriteString(h,"\t\t<td>"+History.Orders[i].OpenTime+"</td>\n");  // Time
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Order+"</td>\n");     // Order 
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Deal 
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Symbol+"</td>\n");    // Symbol 
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Type+"</td>\n");      // Type 
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Direction       
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Volume+"</td>\n");    // Volume    
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Price+"</td>\n");     // Price  
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Order        
   FileWriteString(h,"\t\t<td>"+History.Orders[i].SL+"</td>\n");        // SL
   FileWriteString(h,"\t\t<td>"+History.Orders[i].TP+"</td>\n");        // TP
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Time+"</td>\n");      // Time    
   FileWriteString(h,"\t\t<td>"+History.Orders[i].State+"</td>\n");     // State
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Comission
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Swap
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Profit     
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Ballance    
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Comment+"</td>\n");   // Comment     
   FileWriteString(h,"\t</tr>\n");   
   

Wenn ein Auftrag ausgeführt worden war, suchen wir den entsprechend Deal, bestimmen die Darstellung und erzeugen den HTML-Code:  

// Suchen des Deals

if(History.Orders[i].State=="filled"){
   for(;j<ArraySize(History.Deals);j++){
      if(History.Deals[j].Order==History.Orders[i].Order){
         
         sc="";
         
         if(History.Deals[j].Type=="buy"){
            sc="DealBuy";
         }
         else if(History.Deals[j].Type=="sell"){
            sc="DealSell";
         }
      
         FileWriteString(h,"\t<tr class=\""+sc+"\">\n");   
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Time+"</td>\n");     // Time
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // Order 
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Deal+"</td>\n");     // Deal 
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Symbol+"</td>\n");     // Symbol 
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Type+"</td>\n");     // Type 
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Direction+"</td>\n");     // Direction       
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Volume+"</td>\n");     // Volume    
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Price+"</td>\n");     // Price  
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Order+"</td>\n");     // Order        
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // SL
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // TP
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // Time    
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // State
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Commission+"</td>\n");                    // Comission
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Swap+"</td>\n");     // Swap
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Profit+"</td>\n");     // Profit     
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Balance+"</td>\n");     // Ballance    
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Comment+"</td>\n");     // Comment     
         FileWriteString(h,"\t</tr>\n"); 
         break;
      }
   }
}

Danach schließen wir die Tabelle und ergänzen mit der Funktion HTMLEnd() das standardmäßige Ende einer HTML-Seite:

FileWriteString(h,"</table>\n");   
FileWriteString(h,HTMLEnd("Report"));  

HTMLEnd() function code:

string HTMLEnd(){
   string str="</body>\n";
   str=str+"</html>\n";
   return str;
}

Jetzt müssen wir nur noch die Stile der Datei style.css schreiben. Das Lernen von css geht über den Rahmen dieses Artikels hinaus und deshalb werden wir dies nicht im Detail besprechen. Sie können sich die Datei ansehen, die unten angehängt ist. Außerdem enthält der Anhang das Skript zum Erstellen des Berichts - HTMLReportCreate.mq5.

Hier ist der fertige Bericht:

Der eigene HTML-Bericht
Abb. 5. Ausschnitt aus dem eigenen HTML-Bericht

Schlussfolgerungen

Sie werden sich vielleicht fragen, ob die Verwendung von Regulären Ausdrücken einfacher gewesen wäre. Die Gesamtstruktur und das Prinzip wären die gleichen. Wir würden separat ein Array mit dem Tabelleninhalt, dann der Zeilen und Zellen erhalten. Anstelle von TagsToArray() würden wir eine Funktion mit dem Regulären Ausdruck verwenden. Die übrigen Operationen wären sehr ähnlich.

Das in diesem Artikel beschriebene Beispiel zur Erstellung eines benutzerdefinierten Berichts ist nur eine der Möglichkeiten, Bericht zu erzeugen. Es ist eben nur ein Beispiel. Sie können Ihr eigene, komfortable und verständliche Version entwickeln. Das wichtigste Ergebnis des Artikels ist, dass Sie einen ganz einfachen Zugriff auf alle Berichtsdaten haben.

Anlagen

  • Include/HTMLReport.mqh — einzubindende Datei mit den Funktionen, Berichte zu parsen.
  • Scripts/HTMLReportTest.mq5 — Beispiel einer Verwendung von HTMLReport.mqh zum Parsen der Tests, der Optimierung und der Historienberichte.
  • Scripts/HTMLReportCreate.mq5 — Beispiel einer Erstellung eines nutzerdefinierten HTML-Berichts.
  • Files/ReportTester-555849.html — Bericht des Strategy Testers.
  • Files/ReportOptimizer-555849.xml — Bericht der Optimierung.
  • Files/ReportHistory-555849.html — Bericht der Handelshistorie.
  • Files/Report.htm — Datei des Berichts, die mit dem Skript HTMLReportCreate erstellt wurde.
  • Files/style.css — Stylesheet für Report.htm.