Optimale Farben für Handelsstrategien

Dmitry Fedoseev | 9 Mai, 2019

Einführung

Nach der Optimierung müssen wir nur noch einen aus einer Vielzahl von Parametersätzen auswählen. Es gibt keine klare Antwort darauf, welches Kriterium bei der Auswahl eines solchen Satzes zu verwenden ist: Rentabilität, Absenkung, Erholungsfaktor oder eine Kombination dieser oder anderer Parameter. Aber wie kann man die Kombination der Parameter bewerten?

In diesem Artikel werden wir ein Experiment durchführen: Wir werden die Optimierungsergebnisse einfärben. Die Farbe wird durch drei Parameter bestimmt: die Werte für Rot, Grün und Blau (RGB). Es gibt noch andere Methoden der Farbcodierung, die ebenfalls drei Parameter verwenden. So können drei Prüfparameter in eine Farbe umgewandelt werden, die die Werte visuell darstellt. Am Ende dieses Artikels werden wir herausfinden, ob eine solche Darstellung sinnvoll sein kann.

Ausgangsdaten

in dem Artikel Die Analyse der Handelsergebnisse mit den HTML-Berichten, haben wir eine Bibliothek mit Funktionen, um die Berichtsdateien, HTMLReport.mqh zu analysieren. Die Bibliothek enthält die Funktion OptimizerXMLReportToStruct(), die für das Bearbeiten der Optimierungsergebnisse konzipiert ist. Wir werden diese Funktion verwenden. Es werden zwei Parameter an die Funktion übergeben:

  • string aFileName ist der Name der Datei mit dem Optimierungsbericht. Die Datei muss sich im Ordner MQL5/Files des Datenverzeichnisses des Terminals befinden.
  • SOptimierung & aOptimierung wird als Referenz übergeben. Nach der Ausführung der Funktion befinden sich die aus dem Bericht extrahierten Daten in dieser Struktur.

Die Struktur von SOptimisation:

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

Die Struktur beinhaltet zwei Arrays: string ParameterName[] und SPass Pass[]. Die Namen der zu optimierenden Parameter finden Sie unter ParameterName[]. Wir sind hauptsächlich an dem zweiten Array SPass[] interessiert: Ein Element dieses Arrays enthält Daten über einen Optimierungsdurchlauf.

Die Struktur 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 Felder der Struktur:

  • Pass — die Nummer des Optimierungsdurchlaufs;
  • Result — die Endsaldo nach der Optimierung;
  • Profit — resultierender Gewinn;
  • ExpectedPayoff — der EExpected Payoff;
  • ProfitFactor - der Wert des Profit-Faktor;
  • RecoveryFactor — Erholungsfaktor;
  • SharpeRatio — Sharpe Ratio;
  • Custom — ein nutzerdefinierter Parameter;
  • EquityDD_perc — Drawdown in Prozent;
  • Trades — Anzahl der Positionen;
  • Parameters[] — ein Array mit den Werten der optimierten Parameter.

Nachfolgend finden Sie die gängigsten Parameter für die Analyse von Handelsergebnissen:

  • Rentabilität, der durchschnittliche Gewinn pro Position
  • Drawdown, die Kapitalabfall im Verhältnis zum Maximalwert
  • Erholungsfaktor, das Verhältnis des absoluten Gewinns zum maximalen Drawdown.

Zuerst werden wir diese Parameter verwenden. Der Bericht enthält jedoch auch andere Werte, und deshalb müssen wir die Möglichkeit schaffen, sie zu nutzen. 

Um die beliebige Parameterauswahl zu ermöglichen, werden wir eine zusätzliche Struktur erstellen, die SPass ersetzt. Die Optimierungsparameter werden in einem Array des Typs double innerhalb dieser Struktur angeordnet. Wir werden die Struktur nicht komplett neu schreiben, sondern Vererbungsmöglichkeiten nutzen. Lassen Sie uns mit der Umsetzung fortfahren:

1. Erstellen Sie die Datei ColorOptimization.mqh. In dieser Datei befinden sich alle Funktionen zur Erstellung von farbigen Berichten.

2. Binden Sie die Datei HTMLReport.mqh am Anfang der Datei ColorOptimization.mqh ein:

#include <HTMLReport.mqh>

3. Erstellen Sie eine neue Struktur, die alle Felder der Struktur SPass erbt und fügen Sie die Arrays factor[] und dParameters[] hinzu:

struct SPass2:SPass{
   double factor[9];
   double dParameters[];  
};

Beide Arrays sind vom Typ double. Die neun Ergebniswerte befinden sich im Array Faktor[], d.h. alle außer Pass (Durchlaufnummer) und die zu optimierenden Parametern. Die Werte der Optimierungsparameter befinden sich im Array sParameters[]. Obwohl alle Daten bereits in der Struktur vorhanden sind, werden sie in einem Zeichenkettenformat dargestellt, so dass wir sie bei jeder Verwendung der Daten in Zahlen umwandeln müssten. Das Array ermöglicht es, Daten in einem komfortablen Format zu speichern.

4. Erstellen Sie die endgültige Struktur für die Optimierungsdaten:

struct SOptimization2{
   string ParameterName[];
   SPass2 Pass[];
};

5. Erstellen Sie eine Funktion zur Konvertierung von Daten aus der Struktur SOptimization in SOptimization2:

void ConvertOptimizationStruct(SOptimization & src,SOptimization2 & dst){

   ArrayCopy(dst.ParameterName,src.ParameterName);
   int cnt=ArraySize(src.Pass);
   ArrayResize(dst.Pass,cnt);   
   for(int i=0;i<cnt;i++){
      ArrayCopy(dst.Pass[i].Parameters,src.Pass[i].Parameters);
      
      dst.Pass[i].Pass=src.Pass[i].Pass;
      dst.Pass[i].Result=src.Pass[i].Result;
      dst.Pass[i].Profit=src.Pass[i].Profit;
      dst.Pass[i].ExpectedPayoff=src.Pass[i].ExpectedPayoff;
      dst.Pass[i].ProfitFactor=src.Pass[i].ProfitFactor;
      dst.Pass[i].RecoveryFactor=src.Pass[i].RecoveryFactor;
      dst.Pass[i].SharpeRatio=src.Pass[i].SharpeRatio;
      dst.Pass[i].Custom=src.Pass[i].Custom;
      dst.Pass[i].EquityDD_perc=src.Pass[i].EquityDD_perc;
      dst.Pass[i].Trades=src.Pass[i].Trades;

      dst.Pass[i].factor[0]=StringToDouble(src.Pass[i].Result);
      dst.Pass[i].factor[1]=StringToDouble(src.Pass[i].Profit);
      dst.Pass[i].factor[2]=StringToDouble(src.Pass[i].ExpectedPayoff);
      dst.Pass[i].factor[3]=StringToDouble(src.Pass[i].ProfitFactor);
      dst.Pass[i].factor[4]=StringToDouble(src.Pass[i].RecoveryFactor);
      dst.Pass[i].factor[5]=StringToDouble(src.Pass[i].SharpeRatio);
      dst.Pass[i].factor[6]=StringToDouble(src.Pass[i].Custom);
      dst.Pass[i].factor[7]=StringToDouble(src.Pass[i].EquityDD_perc);
      dst.Pass[i].factor[8]=StringToDouble(src.Pass[i].Trades);
      
      int pc=ArraySize(src.Pass[i].Parameters);
      
      ArrayResize(dst.Pass[i].dParameters,pc);
      
      for(int j=0;j<pc;j++){
         if(src.Pass[i].Parameters[j]=="true"){
            dst.Pass[i].dParameters[j]=1;
         }
         else if(src.Pass[i].Parameters[j]=="false"){
            dst.Pass[i].dParameters[j]=0;         
         }
         else{
            dst.Pass[i].dParameters[j]=StringToDouble(src.Pass[i].Parameters[j]);
         }
      }
   }   
}

Die Datenstruktur mit Daten wird an die Funktion als erster Parameter übergeben und eine neue Struktur wird als zweiter Parameter per Referenz zurückgegeben. In der Funktion wird eine Schleife durch alle Optimierungsdurchläufe durchgeführt; auch einige Felder der Struktur werden kopiert und für einige Felder wird eine Typkonvertierung durchgeführt. Der allgemeine Ablauf ist nicht kompliziert und kann aus dem Funktionscode entnommen werden.

Auf die Array-Elemente von Faktor[] wird über die Enumeration zugegriffen:

enum EOptimizatrionFactor{
   Result=0,
   Profit=1,
   ExpectedPayoff=2,
   ProfitFactor=3,
   RecoveryFactor=4,
   SharpeRatio=5,
   Custom=6,
   EquityDD_perc=7,
   Trades=8
};

Die Werte der Enumeration beginnen bei Null und werden um 1 erhöht, so dass es wahrscheinlich nicht notwendig ist, die Werte anzugeben. Die Werte werden jedoch angegeben, da sie unbedingt zum Array Faktor[] passen müssen. Dies hilft, mögliche Fehler bei weiteren Überarbeitungen und Ergänzungen des Programms zu vermeiden. 

6. Erstellen Sie eine Funktion zum Laden der Berichtsdatei in die Struktur SOptimization2, die ähnlich wie OptimizerXMLReportToStruct() aus HTMLReport.mqh ist:

bool OptimizerXMLReportToStruct2(string aFileName,SOptimization2 & aOptimization){
   SOptimization tmp;
   if(!OptimizerXMLReportToStruct(aFileName,tmp)){
      return(false);
   }
   ConvertOptimizationStruct(tmp,aOptimization);
   return(true);
}

Der Name der Berichtsdatei wird der Funktion als erster Parameter übergeben und die ausgefüllte Struktur SOptimization2 wird über den zweiten Parameter zurückgegeben.

Jetzt ist alles bereit für die Lösung der Hauptaufgabe des Artikels. 

Erstellen eines farbigen Berichts

Die Funktionen zur Erstellung des farbigen Berichts finden Sie in ColorOptimization.mqh. Der Aufruf dieser Funktionen erfolgt über ein Skript.

1. Lassen Sie uns das Skript ColorOptimization.mq5 erstellen.

2. Binden Sie ColorOptimization.mqh in ColorOptimization.mq5 ein.

#include <ColorOptimization.mqh>

3. Fügen Sie dem Skript externe Parameter hinzu: zuerst eine Eigenschaft, die angibt, dass ein Eigenschaftsfenster existiert, und dann die Variablen.

Property:

#property script_show_inputs

Externe Variablen:

input string               ReportName     =  "*.xml";
input string               OutputName     =  "ColorOptimization1-1.htm";
input EOptimizatrionFactor Factor1        =  Profit;
input EOptimizatrionFactor Factor2        =  EquityDD_perc;
input EOptimizatrionFactor Factor3        =  RecoveryFactor;
input bool                 Factor1Invert  =  false;
input bool                 Factor2Invert  =  true;
input bool                 Factor3Invert  =  false;
input bool                 Sort           =  true;

Beschreibung der Variablen:

  • ReportName — der Name der Berichtsdatei für die Quelloptimierung;
  • OutputName — der Dateiname für den Bericht, der vom Skript erstellt wurde;
  • Faktor1 — der erste Faktor, aus dem die Berichtsfarbe bestimmt wird;
  • Faktor2 — der zweite Faktor, aus dem die Berichtsfarbe bestimmt wird;
  • Faktor3 — der dritte Faktor, aus dem die Berichtsfarbe bestimmt wird;
  • Factor1Invert — invertiert den ersten Faktor;
  • Factor2Invert — invertiert den zweiten Faktor;
  • Factor3Invert — invertiert den dritten Faktor;
  • Sort — ermöglicht die Sortierung des Endberichts gemäß der Farbanzeige;

4. In der Funktion OnStart() des Skripts deklarieren wir eine Variable vom Typ SOptimisation2 und erhalten die Quellberichtsdaten:

SOptimization2 opt;

if(!OptimizerXMLReportToStruct2(ReportName,opt)){
   Alert("Error OptimizerXMLReportToStruct2");
   return;
}

5. Da RGB nur eines von vielen verschiedenen Farbmodellen ist, bieten wir die Möglichkeit weiterer Bibliotheksmodifikationen, insbesondere die Erweiterung mit weiteren Farbmodellen. Deshalb beginnen wir mit der Berechnung von abstrakten Werten von 0 bis 1 und nicht mit der Berechnung der Werte von RGB-Komponenten. Dann werden wir diese Werte in RGB-Komponenten im Bereich von 0 bis 255 umwandeln. Für jeden Optimierungsdurchlauf wird eine separate Farbanzeige verwendet, daher müssen wir SPass2 drei Felder für Farbkomponenten hinzufügen. Anstatt drei Felder hinzuzufügen, werden wir ein Array mit drei Elementen hinzufügen:

double ColorComponent[3];

6. Die Funktion SolveColorComponents() in ColorOptimization.mqh berechnet die Farbkomponenten. Die folgenden Parameter sollten an die Funktion übergeben werden:

  • SOptimization2 & aOpt — Daten des Quellenoptimierungsberichts
  • int i1, int i2, int i3 — Indizes der Werte des Quell-Optimierungsberichts (Faktor[9] Array der Struktur SPass)
  • bool r1=false, bool r2=false, bool r3=false — Variablen zur Invertierung der Werte

Nach der Ausführung der Funktion werden in das Array ColorComponents[3] im Array der Struktur SPass die Werte eingetragen. 

Für die Berechnung von farbigen Komponenten müssen wir für jeden der Parameter den Minimal- und Maximalwert finden und dann den Wert im Bereich von 0 bis 1 berechnen. Der gesamte Funktionscode von SolveColorComponents() wird unten angezeigt:

void SolveColorComponents(  SOptimization2 & aOpt,
                              int i1,int i2,int i3,
                              bool r1=false,bool r2=false,bool r3=false){
   
   double mx[3]={0,0,0};
   double mn[3]={DBL_MAX,DBL_MAX,DBL_MAX};
   
   int size=ArraySize(aOpt.Pass);
   
   for(int i=0;i<size;i++){
      mx[0]=MathMax(mx[0],aOpt.Pass[i].factor[i1]);
      mx[1]=MathMax(mx[1],aOpt.Pass[i].factor[i2]);
      mx[2]=MathMax(mx[2],aOpt.Pass[i].factor[i3]);
      mn[0]=MathMin(mn[0],aOpt.Pass[i].factor[i1]);
      mn[1]=MathMin(mn[1],aOpt.Pass[i].factor[i2]);
      mn[2]=MathMin(mn[2],aOpt.Pass[i].factor[i3]);      
   }

   double c1,c2,c3,d;
   
   for(int i=0;i<size;i++){      
   
      c1=0;
      c2=0;
      c3=0;
   
      d=mx[0]-mn[0];
      if(d!=0){
         c1=(aOpt.Pass[i].factor[i1]-mn[0])/d;
      }
      
      d=mx[1]-mn[1];
      if(d!=0){
         c2=(aOpt.Pass[i].factor[i2]-mn[1])/d; 
      }
      
      d=mx[2]-mn[2];
      if(d!=0){
         c3=(aOpt.Pass[i].factor[i3]-mn[2])/d;       
      }
      
      if(r1)c1=1.0-c1;
      if(r2)c2=1.0-c2;
      if(r3)c3=1.0-c3;
      
      aOpt.Pass[i].ColorComponent[0]=c1;
      aOpt.Pass[i].ColorComponent[1]=c2;      
      aOpt.Pass[i].ColorComponent[2]=c3;   
   }

}

Und so wird diese Funktion in einem Skript aufgerufen:

SolveColorComponents(opt,Factor1,Factor2,Factor3,Factor1Invert,Factor2Invert,Factor3Invert);

7. Wenn das Sortieren in den externen Skriptparametern aktiviert ist, müssen wir den Faktor für die Sortierung berechnen und diese Sortierung durchführen. Der beste Optimierungsdurchlauf ist derjenige, bei dem alle Parameter zusammen den Maximalwert haben. Wenn diese Parameter RGB entsprechen, ist die beste Option die weiße Farbe. Daher sollte der Sortierfaktor als arithmetisches Mittel aus drei Komponenten berechnet werden.

Fügen wir der SPass2-Struktur ein weiteres Feld hinzu:

double SortFactor; 

Die Funktionen zur Berechnung des Sortierfaktors sollten der Datei ColorOptimization.mqh hinzugefügt werden:

void SolveSortFactor(SOptimization2 & aOpt){

   int size=ArraySize(aOpt.Pass);
   
   for(int i=0;i<size;i++){
      aOpt.Pass[i].SortFactor=0;
      for(int j=0;j<3;j++){
         aOpt.Pass[i].SortFactor+=aOpt.Pass[i].ColorComponent[j];
      }
      aOpt.Pass[i].SortFactor/=3;
   }
}

Unten ist die Sortierfunktion (verwendet wird der Bubble-Sort)

void SortFactorSort(SOptimization2 & aOpt){
   int size=ArraySize(aOpt.Pass);
   for(int i=size-1;i>0;i--){
      for(int j=0;j<i;j++){
         if(aOpt.Pass[j].SortFactor<aOpt.Pass[j+1].SortFactor){
            SPass2 tmp=aOpt.Pass[j];
            aOpt.Pass[j]=aOpt.Pass[j+1];
            aOpt.Pass[j+1]=tmp;
         }
      }
   }
}

Rufen wir diese Funktionen im Skript auf. Der Sortierfaktor wird nicht nur zum Sortieren der Tabelle verwendet, daher wird der SolveSortFactor() unabhängig vom Wert der Sort Variable aufgerufen:

SolveSortFactor(opt);
if(Sort){   
   SortFactorSort(opt);
}

Jetzt ist alles bereit für die Berichtserstellung. Der Bericht wird aus zwei Teilen bestehen. Die erste ist eine Kopie der Optimierungsdatentabelle mit einer zusätzlichen Farbtaste (Abb. 1.). Der zweite Teil besteht aus mehreren farbigen Ebenen (Tabellen) für jedes Paar optimierter Parameter, wobei jede Zelle den Gradienten anzeigt, der Änderungen in den Testergebnissen für das jeweilige Paar optimierter Parameter widerspiegelt (Abb. 2).

Tabelle mit einer Farbanzeige

Das Erstellen einer Tabelle mit einer zusätzlichen Farbanzeige erfolgt in der Funktion TableContent(). Diese Funktion befindet sich in der Datei ColorOptimization.mqh und gibt den HTML-Code der Tabelle zurück.

Die Erstellung einer HTML-Tabelle ist eine einfache Aufgabe. Die Farbe für eine Farbanzeigezelle wird durch die Angabe des Zellstils, des Attributs 'background-color', festgelegt. Farbkomponenten im Bereich von 0 bis 1 können durch Multiplikation der Werte leicht in Komponenten im Bereich von 1 bis 255 umgewandelt werden. Um mehr visuelle Informationen in der Tabelle zu liefern, lassen Sie uns Details darüber hinzufügen, welcher Optimierungsparameter dieser oder jener Farbe entspricht. Diese Daten werden in der oberen Zelle der Farbindikatorspalte angegeben, und die oberen Zellen der Parameter haben die entsprechende Farbe (Abb. 1).

Berichtsfragment mit Farbanzeige
Abb. 1. Berichtsfragment mit Farbanzeige

Die ganze Funktion TableContent() folgt unten:

string TableContent(SOptimization2 & aOpt,int i1,int i2,int i3){
   
   int size=ArraySize(aOpt.Pass);
     
   int pc=ArraySize(aOpt.ParameterName);

   int nc=ArraySize(co_names);
   
   string s="<table>";
   
   s=s+"<tr>";
   s=s+"<th>Pass</td>";
   
   for(int i=0;i<nc;i++){
      s=s+"<th"+HStyle(i,i1,i2,i3)+">"+co_names[i]+"</th>";   
   }
   
   s=s+"<th>"+ColorCollHeader(i1,i2,i3)+"</th>";  
   
   for(int j=0;j<pc;j++){
      s=s+"<th>"+aOpt.ParameterName[j]+"</th>";       
   }
   s=s+"</tr>";     
   
   int r,g,b;
   
   for(int i=0;i<size;i++){    
   
      ComponentsToRGB(aOpt.Pass[i].ColorComponent[0],
                      aOpt.Pass[i].ColorComponent[1],
                      aOpt.Pass[i].ColorComponent[2],
                      r,g,b);
   
      s=s+"<tr>";
   
      s=s+"<td>"+aOpt.Pass[i].Pass+"</td>";
      s=s+"<td>"+aOpt.Pass[i].Result+"</td>";   
      s=s+"<td>"+aOpt.Pass[i].Profit+"</td>";         
      s=s+"<td>"+aOpt.Pass[i].ExpectedPayoff+"</td>";   
      s=s+"<td>"+aOpt.Pass[i].ProfitFactor+"</td>";               
      s=s+"<td>"+aOpt.Pass[i].RecoveryFactor+"</td>";        
      s=s+"<td>"+aOpt.Pass[i].SharpeRatio+"</td>";               
      s=s+"<td>"+aOpt.Pass[i].Custom+"</td>";   
      s=s+"<td>"+aOpt.Pass[i].EquityDD_perc+"</td>";        
      s=s+"<td>"+aOpt.Pass[i].Trades+"</td>";               
      
      string cs=RGBToStr(r,g,b);
      s=s+"<td title='"+cs+"' style='background-color: "+cs+"'>&nbsp</td>";        
      
      for(int j=0;j<pc;j++){
         s=s+"<td>"+aOpt.Pass[i].Parameters[j]+"</td>";       
      }

      s=s+"</tr>";   
   
   }
   
   s=s+"</table>";   

   return(s);
   
}

Betrachten wir diese Funktion näher. Wir erhalten die Anzahl der Optimierungsdurchläufe in der Variablen 'size', die Anzahl der optimierten Parameter wird in der Variablen pc empfangen und die Größe des Arrays mit den Parameternamen (die auf globaler Ebene deklariert wird) wird in der Variablen nc empfangen:

int size=ArraySize(aOpt.Pass);
     
int pc=ArraySize(aOpt.ParameterName);

int nc=ArraySize(co_names);

Das globale Array co_names[]:

string co_names[]={"Result","Profit","Expected Payoff",
                   "Profit Factor","Recovery Factor",
                   "Sharpe Ratio","Custom","Equity DD","Trades"};

Der HTML-Code der Tabelle wird der Variablen s während ihrer Bildung hinzugefügt, so dass wir bei der Deklaration der Variablen das Tag Tabellenanfang hinzufügen:

string s="<table>";

Dann fügen wir das Zeilenanfang-Tag und die erste Zelle der Kopfzeile mit dem Text "Pass" hinzu:

s=s+"<tr>";
s=s+"<th>Pass</th>";

Auf die Spalte "Pass" folgen die Spalten mit Parametern, von denen jeder zur Bildung der Farbanzeige verwendet werden kann. Festschreiben des HTML-Codes für die Zellen:

for(int i=0;i<nc;i++){
   s=s+"<th"+HStyle(i,i1,i2,i3)+">"+co_names[i]+"</th>";   
}

Falls erforderlich, bildet die Funktion HStyle() einen Code, der die Hintergrundfarbe der Zelle ändert:

string HStyle(int i,int i1,int i2,int i3){
   if(i==i1)return(" style='background-color: rgb(255,0,0);'");
   if(i==i2)return(" style='background-color: rgb(0,255,0);'");
   if(i==i3)return(" style='background-color: rgb(0,0,255);'");
   return("");
}

Bilden wir jetzt die Zelle mit der Farbgebung des Headers:

s=s+"<th>"+ColorCollHeader(i1,i2,i3)+"</th>";

Die Funktionscode von ColorCollHeader():

string ColorCollHeader(int i1,int i2,int i3){
   return(co_names[i1]+"-R,<br>"+co_names[i2]+"-G,<br>"+co_names[i3]+"-B");
}

Dann bilden wir den HTML-Code für die Zellen, die die Namen der optimierten Parameter enthalten und beenden die Tabellenzeile:

for(int j=0;j<pc;j++){
   s=s+"<th>"+aOpt.ParameterName[j]+"</th>";       
}
s=s+"</tr>";     

Danach werden drei Hilfsvariablen deklariert: r, g, b. Es folgt eine Schleife, in der der HTML-Code aller Berichtszeilen gebildet wird. Die RGB-Komponentenwerte werden am Anfang jeder Schleife berechnet:

ComponentsToRGB(aOpt.Pass[i].ColorComponent[0],
                aOpt.Pass[i].ColorComponent[1],
                aOpt.Pass[i].ColorComponent[2],
                r,g,b);

Der Funktionscode von ComponentsToRGB()

void ComponentsToRGB(double c1,double c2,double c3,int & r,int & g,int & b){
   r=(int)(c1*255.0);
   g=(int)(c2*255.0);
   b=(int)(c3*255.0);
}

Dann wird der HTML-Code einer Zeile mit den Zellen gebildet, die Testergebnisse enthalten:

s=s+"<tr>";
   
s=s+"<td>"+aOpt.Pass[i].Pass+"</td>";
s=s+"<td>"+aOpt.Pass[i].Result+"</td>";   
s=s+"<td>"+aOpt.Pass[i].Profit+"</td>";         
s=s+"<td>"+aOpt.Pass[i].ExpectedPayoff+"</td>";   
s=s+"<td>"+aOpt.Pass[i].ProfitFactor+"</td>";               
s=s+"<td>"+aOpt.Pass[i].RecoveryFactor+"</td>";        
s=s+"<td>"+aOpt.Pass[i].SharpeRatio+"</td>";               
s=s+"<td>"+aOpt.Pass[i].Custom+"</td>";   
s=s+"<td>"+aOpt.Pass[i].EquityDD_perc+"</td>";        
s=s+"<td>"+aOpt.Pass[i].Trades+"</td>";      

Dann kommt die Farbgebung der Zelle. Die RGB-Komponenten werden zunächst mit der Funktion RGBToStr() in eine Zeichenkette umgewandelt; danach wird der Zellcode gebildet:

string cs=RGBToStr(r,g,b);
s=s+"<td title='"+cs+"' style='background-color: "+cs+"'>&nbsp</td>"; 

Funktionscode von RGBToStr()

string RGBToStr(int r,int g,int b){
   return("rgb("+(string)r+","+(string)g+","+(string)b+")");
}

Zellen mit den Werten der zu optimierenden Parameter werden am Ende der Zeile angezeigt:

for(int j=0;j<pc;j++){
   s=s+"<td>"+aOpt.Pass[i].Parameters[j]+"</td>";       
}

s=s+"</tr>"

Die Tabelle wird geschlossen und der Inhalt der Variablen s wird am Ende der Funktion zurückgegeben:

s=s+"</table>";   

return(s);

Die Hintergründe der optimierten Parameter

Der Hintergrund kann gezeichnet werden, wenn es zwei oder mehr optimierte Parameter gibt. Der Hintergrund ist in Abb. 2 dargestellt.


Abb. 2. Optimierte Parameter für die Hintergründe

Die erste Zeile zeigt, welche Parameter den Hintergrundachsen entsprechen: Werte von Inp_Signal_MACD_PeriodSlow werden entlang der X-Achse (horizontal) und Inp_Signal_MACD_PeriodFast Werte werden entlang der Y-Achse angezeigt. Der Gradient in den Zellen zeigt, wie sich die Testergebnisse für dieses Wertepaar von X- und Y-Parametern geändert haben, wenn andere Parameter geändert wurden. Die Farbe des schlechtesten Wertes wird links und die des besten von rechts angezeigt. Die Bestimmung der besten und schlechtesten Variante erfolgt auf Basis des zuvor genannten Sortierfaktors, der als arithmetisches Mittel der abstrakten Farbkomponenten berechnet wird.

Der HTML-Code des Hintergrunds wird in der Funktion Color2DPlanes() gebildet. In dieser Funktion werden alle möglichen Kombinationen von zwei optimierten Parametern gefunden, und der HTML-Code des Hintergrunds wird für jedes Paar generiert. Der Funktionscode von Color2DPlanes():

string Color2DPlanes(SOptimization2 & aOpt){
   string s="";
   int pc=ArraySize(aOpt.ParameterName);
   for(int y=0;y<pc;y++){
      for(int x=y+1;x<pc;x++){
         s=s+Color2DPlane(aOpt,x,y);         
      }   
   }
   return(s);
}

Der HTML-Code einer Ebene wird in der Funktion Color2DPlane() gebildet:

string Color2DPlane(SOptimization2 & aOpt,int xi,int yi){

   double xa[];
   double ya[];
   
   int cnt=ArraySize(aOpt.Pass);

   ArrayResize(xa,cnt);
   ArrayResize(ya,cnt);
   
   for(int i=0;i<cnt;i++){
      xa[i]=aOpt.Pass[i].dParameters[xi];
      ya[i]=aOpt.Pass[i].dParameters[yi];      
   }
   
   ArraySort(xa);
   ArraySort(ya);
   
   int xc=1;
   int yc=1;
   
   for(int i=1;i<cnt;i++){
      if(xa[i]!=xa[i-1]){
         xa[xc]=xa[i];
         xc++;
      }
      if(ya[i]!=ya[i-1]){
         ya[xc]=ya[i];
         yc++;
      }
   }   

   string s="<hr><h3>X - "+aOpt.ParameterName[xi]+", Y - "+aOpt.ParameterName[yi]+"</h3><table>";


   s=s+"<tr>";   
      s=s+"<td>&nbsp;</td>";
      for(int x=0;x<xc;x++){
         s=s+"<td>"+(string)xa[x]+"</td>";
      }
   s=s+"</tr>";   
   for(int y=0;y<yc;y++){
      
      s=s+"<tr>";
      
      s=s+"<td>"+(string)ya[y]+"</td>";
      for(int x=0;x<xc;x++){

         double mx=0;
         double mn=DBL_MAX;
         int mxi=0;
         int mni=0; 
         
         for(int i=0;i<cnt;i++){
            if(aOpt.Pass[i].dParameters[yi]==ya[y] && 
               aOpt.Pass[i].dParameters[xi]==xa[x]
            ){
               if(aOpt.Pass[i].SortFactor>mx){
                  mx=aOpt.Pass[i].SortFactor;
                  mxi=i;
               }
               if(aOpt.Pass[i].SortFactor<mn){
                  mn=aOpt.Pass[i].SortFactor;
                  mni=i;
               }
            }
         }
         
         int mnr,mng,mnb;
         int mxr,mxg,mxb;
         
         ComponentsToRGB(aOpt.Pass[mni].ColorComponent[0],
                         aOpt.Pass[mni].ColorComponent[1],
                         aOpt.Pass[mni].ColorComponent[2],
                         mnr,mng,mnb);
                         
         ComponentsToRGB(aOpt.Pass[mxi].ColorComponent[0],
                         aOpt.Pass[mxi].ColorComponent[1],
                         aOpt.Pass[mxi].ColorComponent[2],
                         mxr,mxg,mxb);         
        
         string title=RGBToStr(mnr,mng,mnb)+"/"+RGBToStr(mxr,mxg,mxb)+"\n";
               
         int digits[]={2,2,6,6,6,6,6,4,0};

         for(int k=0;k<ArraySize(co_names);k++){
            title=title+co_names[k]+": "+DoubleToString(aOpt.Pass[mni].factor[k],digits[k])+
            "/"+DoubleToString(aOpt.Pass[mxi].factor[k],digits[k])+"\n";
         }

         s=s+"<td title='"+title+"' style='background: linear-gradient(to right, rgb("+
         (string)mnr+","+(string)mng+","+(string)mnb+"), rgb("+
         (string)mxr+","+(string)mxg+","+(string)mxb+"));'>&nbsp;"+
         (string)mni+"-"+(string)mxi+"</td>";
      }
      s=s+"</tr>";
   }
   
   s=s+"<table>";   

   return(s);

}

Betrachten wir die Color2DPlane() Funktion genauer. Folgendes wird in die Funktion eingegeben: SOptimization2-Struktur, die alle Daten aus dem Optimierungsbericht und zwei int Variablen xi und yi enthält, die die Indizes des Paares der optimierten Parameter sind, für die eine Ebene erstellt wird. Zuerst werden wir alle möglichen Werte jedes der beiden Parameterpaare in Arrays sammeln. Dazu deklarieren wir zwei Arrays, ändern ihre Größe entsprechend der Anzahl der Optimierungsdurchläufe und füllen sie mit allen möglichen Werten:

double xa[];
double ya[];

int cnt=ArraySize(aOpt.Pass);

ArrayResize(xa,cnt);
ArrayResize(ya,cnt);

for(int i=0;i<cnt;i++){
   xa[i]=aOpt.Pass[i].dParameters[xi];
   ya[i]=aOpt.Pass[i].dParameters[yi];      
}

Es sollten nur eindeutige Werte von Parametern verwendet werden, also lassen Sie uns die Arrays sortieren und eindeutige Werte an den Anfang der Arrays verschieben:

ArraySort(xa);
ArraySort(ya);

int xc=1;
int yc=1;

for(int i=1;i<cnt;i++){
   if(xa[i]!=xa[i-1]){
      xa[xc]=xa[i];
      xc++;
   }
   if(ya[i]!=ya[i-1]){
      ya[xc]=ya[i];
      yc++;
   }
}   

Danach enthält die Variable xc die Anzahl der eindeutigen Werte eines Parameters und yc die des anderen Parameters. Der HTML-Code des Hintergrunds wird während der Bildung der Variablen s hinzugefügt. Lassen Sie uns Informationen über die Namen der Variablen und das Tag zum Öffnen der Tabelle sofort während der Variablendeklaration s hinzufügen:

string s="<hr><h3>X - "+aOpt.ParameterName[xi]+", Y - "+aOpt.ParameterName[yi]+"</h3><table>";

Erstellen wir die erste Tabellenzeile mit den x-Parameterwerten:

s=s+"<tr>";   
   s=s+"<td>&nbsp;</td>";
   for(int x=0;x<xc;x++){
      s=s+"<td>"+(string)xa[x]+"</td>";
   }
s=s+"</tr>"; 

Nach der Schleife über alle Varianten des Parameters y:

for(int y=0;y<yc;y++){

In der Schleife beginnen wir eine Zeile bei jedem Durchlauf und ergänzen eine Zelle mit dem Parameterwert y:

s=s+"<tr>";
      
s=s+"<td>"+(string)ya[y]+"</td>";

Dann fügen wir Zellen mit dem Gradienten hinzu (sie werden in einer Schleife über alle Parametervarianten x hinzugefügt):

for(int x=0;x<xc;x++){

Um einen Gradienten zu erzeugen, ist es notwendig, die besten und schlechtesten Optimierungsdurchläufe zu finden:

double mx=0;
double mn=DBL_MAX;
int mxi=0;
int mni=0; 

for(int i=0;i<cnt;i++){
   if(aOpt.Pass[i].dParameters[yi]==ya[y] && 
      aOpt.Pass[i].dParameters[xi]==xa[x]
   ){
      if(aOpt.Pass[i].SortFactor>mx){
         mx=aOpt.Pass[i].SortFactor;
         mxi=i;
      }
      if(aOpt.Pass[i].SortFactor<mn){
         mn=aOpt.Pass[i].SortFactor;
         mni=i;
      }
   }
}

Nach der Ausführung dieses Codeteils enthalten die Variablen mxi und mni die Indizes des besten und schlechtesten Optimierungsdurchgangs. 

Abstrakte Farbkomponenten müssen dann in RGB umgewandelt werden:

ComponentsToRGB(aOpt.Pass[mni].ColorComponent[0],
                aOpt.Pass[mni].ColorComponent[1],
                aOpt.Pass[mni].ColorComponent[2],
                mnr,mng,mnb);
                         
ComponentsToRGB(aOpt.Pass[mxi].ColorComponent[0],
                aOpt.Pass[mxi].ColorComponent[1],
                aOpt.Pass[mxi].ColorComponent[2],
                mxr,mxg,mxb);  

Für eine effizientere Analyse der Hintergründe können wir Tooltips hinzufügen (kann über das HTML-Attribut 'title' hinzugefügt werden):

string title=RGBToStr(mnr,mng,mnb)+"/"+RGBToStr(mxr,mxg,mxb)+"\n";
      
int digits[]={2,2,6,6,6,6,6,4,0};

for(int k=0;k<ArraySize(co_names);k++){
   title=title+co_names[k]+": "+DoubleToString(aOpt.Pass[mni].factor[k],digits[k])+
   "/"+DoubleToString(aOpt.Pass[mxi].factor[k],digits[k])+"\n";
}

Die Titel werden in Abb. 3 gezeigt.


Abb. 3. Ein Tooltip für den Hintergrund einer Zelle

Der Tooltip enthält alle Daten über den schlechtesten und besten Optimierungslauf (den schlechtesten / besten). Die Komponentenwerte des RGB-Gradienten werden in der ersten Zeile des Tooltips angezeigt. 

Nun geht es zum wichtigsten Teil, zum Gradienten:

s=s+"<td title='"+title+"' style='background: linear-gradient(to right, rgb("+
(string)mnr+","+(string)mng+","+(string)mnb+"), rgb("+
(string)mxr+","+(string)mxg+","+(string)mxb+"));'>&nbsp;"+
(string)mni+"-"+(string)mxi+"</td>";

Die Gradientenanzeige wurde in den folgenden Webbrowsern überprüft: Opera, Google Chrome, Yandex Browser und Microsoft-Edge. Mit allen funktionierte es gut.

Fügen wir noch das Zeilenende-Tag am Ende jeder Zeile hinzu:

s=s+"</tr>";

Am Ende fügen wir der Tabelle das Tag Tabellen-Ende hinzu und geben den gebildeten HTML-Code zurück:

s=s+"<table>";   

return(s);

Jetzt rufen wir die Funktionen aus dem Skript auf:

string report=HTMLStart("Color Optimization","style2.css")+
TableContent(opt,Factor1,Factor2,Factor3)+
Color2DPlanes(opt)+HTMLEnd();
    

Ich habe die Funktionen HTMLStart() und HTMLEnd() aus dem Artikel Die Analyse der Handelsergebnisse mit den HTML-Berichten verwendet. Die Datei styles aus dem gleichen Artikel wurde leicht geändert und in style2.css umbenannt.

Die fertigen Dateien sind unten angehängt: ColorOptimization.mqh und das Skript ColorOptimization.mq5. 

Änderung des Farbmodells

Der Code in ColorOptimization.mqh ist so strukturiert, dass Sie ihn leicht für ein anderes Farbmodell ändern können. Versuchen wir, das CMY-Farbmodell hinzuzufügen. Dazu müssen wir einige Vorarbeiten treffen.

1. Wir kopieren ColorOptimization.mqh und ColorOptimization.mq5 und speichern Sie sie als ColorOptimization2.mqh und ColorOptimization2.mq5 

2. Und fügen zu ColorOptimization2.mqh zwei Konstanten für die beiden Farbmodelltypen und eine globale Variable hinzu, die das Farbmodell bestimmt:

#define MODEL_RGB 0
#define MODEL_CMY 1

int co_ColorModel;

3. Wir fügen eine Enumeration und eine externe Variable hinzu, mit der der Benutzer das Farbmodell auswählen kann:

enum EColorModel{
   RGB=MODEL_RGB,
   CMY=MODEL_CMY
};

input EColorModel          ColorModel     =  RGB;

Wir weisen zu Beginn der Funktion OnStart() des Skripts den im Eigenschaftenfenster ausgewählten Wert der Variablen co_ColorModel zu:

co_ColorModel=ColorModel;

Die wichtigsten Änderungen werden in den Dateifunktionen ColorOptimization2.mqh vorgenommen. Zuerst müssen wir ComponentsToRGB() ändern. Die Werte der Komponenten im CMY-Modell reichen von 0 bis 1 und damit entsprechen die Werte der Komponenten der Berichtsdatenstruktur den CMY-Komponenten und können auf RGB umgerechnet werden. Hier ist die Struktur von ComponentsToRGB():

void ComponentsToRGB(double c1,double c2,double c3,int & r,int & g,int & b){
   if(co_ColorModel==MODEL_RGB){
      r=(int)(c1*255.0);
      g=(int)(c2*255.0);
      b=(int)(c3*255.0);
   }
   else if(co_ColorModel==MODEL_CMY){
      CMYtoRGB(c1,c2,c3,r,g,b);
   }
}

Die Transformation des CMY-Modells nach RGB ist in einer separaten Funktion implementiert:

void CMYtoRGB(double C,double M,double Y,int & R,int & G,int & B){
   R=(int)((1.0-C)*255.0);
   G=(int)((1.0-M)*255.0);
   B=(int)((1.0-Y)*255.0);
}

Andere Änderungen betreffen nur zusätzliche Berichtselemente. Überarbeitung der Funktion HStyle() für eine korrekte Einfärbung der Zellen der Kopfzeile der Tabelle:

string HStyle(int i,int i1,int i2,int i3){
   if(co_ColorModel==MODEL_RGB){
      if(i==i1)return(" style='background-color: rgb(255,0,0);'");
      if(i==i2)return(" style='background-color: rgb(0,255,0);'");
      if(i==i3)return(" style='background-color: rgb(0,0,255);'");
   }
   else if(co_ColorModel==MODEL_CMY){
      if(i==i1)return(" style='background-color: rgb(0,255,255);'");
      if(i==i2)return(" style='background-color: rgb(255,0,255);'");
      if(i==i3)return(" style='background-color: rgb(255,255,0);'");      
   }
   return("");
}

Überarbeitung der Funktion ColorCollHeader() um eine korrekte Kopfzeile der Farbanzeige-Spalte:

string ColorCollHeader(int i1,int i2,int i3){
   if(co_ColorModel==MODEL_RGB){
      return(co_names[i1]+"-R,<br>"+co_names[i2]+"-G,<br>"+co_names[i3]+"-B");
   }
   else if(co_ColorModel==MODEL_CMY){
      return(co_names[i1]+"-C,<br>"+co_names[i2]+"-M,<br>"+co_names[i3]+"-Y");   
   }
   return "";
}

Dann müssen einige Änderungen an den Tooltips der Haupttabelle und der Farbebenen vorgenommen werden. Für die Haupttabelle müssen wir den Wert des Attributs 'title' im TableContent() ändern. Die folgenden Zeilen:

string cs=RGBToStr(r,g,b);
s=s+"<td title='"+cs+"' style='background-color: "+cs+"'>&nbsp</td>";   

sollten wie folgt geändert werden:

string ts="",cs=RGBToStr(r,g,b);

if(co_ColorModel==MODEL_RGB){    
   ts=cs;
}
else if(co_ColorModel==MODEL_CMY){
   ts=CMYToStr(aOpt.Pass[i].ColorComponent[0],
               aOpt.Pass[i].ColorComponent[1],
               aOpt.Pass[i].ColorComponent[2]);
}
s=s+"<td title='"+ts+"' style='background-color: "+cs+"'>&nbsp</td>";     

The 'title' attribute in the Color2DPlane() function should be changed to set proper titles for planes. The line:

string title=RGBToStr(mnr,mng,mnb)+"/"+RGBToStr(mxr,mxg,mxb)+"\n";

should be changes as follows:

string title="";

if(co_ColorModel==MODEL_RGB){
   title=RGBToStr(mnr,mng,mnb)+"/"+RGBToStr(mxr,mxg,mxb)+"\n";
}
else if(co_ColorModel==MODEL_CMY){         
   title=CMYToStr(aOpt.Pass[mni].ColorComponent[0],
                  aOpt.Pass[mni].ColorComponent[1],
                  aOpt.Pass[mni].ColorComponent[2])+"/"+
         CMYToStr(aOpt.Pass[mxi].ColorComponent[0],
                  aOpt.Pass[mxi].ColorComponent[1],
                  aOpt.Pass[mxi].ColorComponent[2])+"\n";                            
}

Nun kann der Farbmodelltyp beim Start des Skripts ausgewählt werden. Der Unterschied zwischen CMY und RGB ist größer als bei CMY, die besten Werte werden in Schwarz und andere Farben werden ebenfalls unterschiedlich sein (Abbildungen 4, 5).


Abb. 4. Teil des Berichts erstellt mit dem Farbmodell CMY


Abb. 5. Die Hintergrundfarbe im Farbmodell CMY

Interpretation der angezeigten Farben

Die besten Optionen in RGB sind nahe an Weiß und in CMY nahe an Schwarz. Für eine korrekte Interpretation anderer Farben müssen wir verstehen, wie einzelne Komponenten innerhalb des Farbmodells kombiniert werden und wie die resultierende Farbe gebildet wird.

Lassen Sie uns das RGB-Modell genauer betrachten. Wenn die Werte aller Komponenten gleich 0 sind, erhalten wir Schwarz. Wenn alle Komponenten gleich dem Maximalwert sind, ist Weiß. Alle anderen Kombinationen bieten unterschiedliche Farben. Wenn eine der Komponenten den höchsten Wert hat und die anderen beiden gleich 0 sind, erhalten wir die klare Farbe der entsprechenden Komponente: rot, grün oder blau. Wenn zwei Komponenten Maximalwerte haben und die dritte Komponente Null ist, ist auch die resultierende Farbe klar. Rot und grün ergeben Gelb, Grün und Blau ergeben Cyan, Rot und Blau werden als Magenta dargestellt. Abbildung 6 zeigt mehrere Kombinationen von RGB-Komponenten.


Abb. 7. Basiskombinationen von RGB-Komponenten

Anhand des Farbtons können wir erkennen, welcher Parameterindikator das Testergebnis verbessert. Bei Rot, ist es der erste Parameter; bei Gelb ist es der erste und zweite, Grün steht für den dritten Parameter, etc.

Farben im RGB-Modell werden ähnlich wie farbige Leuchten hinzugefügt. Im CMY-Modell werden die Werte von Weiß subtrahiert, so dass der Maximalwert aller Komponenten Schwarz entspricht. Das CMY-Modell ähnelt dem Mischen von Farben: Wenn es keine Farben gibt, dann sehen wir ein weißes Blatt; wenn zu viele verschiedene Farben gemischt wurden, wird es Schwarz (oder besser gesagt eine schmutzige Farbe bei echten Farben). Bild 8. zeigt die Basiskombinationen der CMY-Komponenten.


Abb. 7. Basiskombinationen der CMY-Komponenten.

Die Farben in CMY sind im Vergleich zu RGB verschoben. Hier ist die Interpretation: Cyan ist der erste Parameter, Blau bedeutet der erste und der zweite, Magenta steht für den zweiten, Rot steht für den zweiten und dritten Wert, Gelb für den dritten und Grün für den ersten und dritten Parameter.

Wie Sie sehen können, gibt es keinen grundlegenden Unterschied bei der Verwendung von RGB- oder CMY-Modellen. 

Schlussfolgerung

Die Wahrnehmung von Farben ist ein subjektiver Prozess und es ist daher schwierig, klare Aussagen über die Anschaulichkeit und den Nutzen der Farbdarstellung zu treffen. Mindestens eine optische Anzeige, die Helligkeit (d.h. die Nähe zu Weiß in RGB), ermöglicht die Bewertung der Kombination der drei Parameter. Dies kann die Berichtsanalyse vereinfachen. Wenn die Auswahl automatisiert erfolgt, wie in diesem Artikel, wird die Entscheidung auf der Grundlage einer sortierten Tabelle nach dem arithmetischen Mittel der drei Werte getroffen. Dies kann als der erste Schritt in den Bereich der Fuzzy-Logik angesehen werden, mit dem der Endwert nicht als einfaches arithmetisches Mittel, sondern auf komplexere Weise berechnet werden kann. Wir brauchen jedoch mehr praktische Experimente, um die Wirksamkeit dieser Methode zu bewerten.

Anlagen

Alle Dateien mit Ausnahme der von den Skripten erstellten Berichte sind in die Ordner zu kopieren, wie sie sich in den Terminalordnern befinden sollten. Öffnen Sie den Terminaldatenordner und kopieren Sie den Ordner MQL5 dahinein.