Das MQL5-Kochbuch: Speichern der Optimierungsergebnisse eines Expert Advisors auf Basis bestimmter Kriterien

Anatoli Kazharski | 27 Juni, 2016

Einleitung

Wir setzen die Serie der Beiträge zur MQL5-Programmierung fort. Diesmal sehen wir uns an, wie man bei der Optimierung der Parameter eines Expert Advisors Ergebnisse erhält. Mit der Umsetzung wird sichergestellt, dass die Werte des entsprechenden Durchlaufs in eine Datei geschrieben werden, wenn eine bestimmte in den externen Parametern festgelegte Bedingung erfüllt wird. Neben Testwerten speichern wir auch die Parameter, die zu diesen Ergebnissen geführt haben.

 

Entwicklung

Zur Umsetzung der Idee verwenden wir den vorgefertigten Expert Advisor mit einfachem Handelsalgorithmus aus dem Beitrag "Das MQL5-Kochbuch: Wie vermeidet man Fehler beim Einstellen/Modifizieren von Handelsebenen" und fügen einfach alle erforderlichen Funktionen hinzu. Der Quellcode wurde mithilfe der in den letzten Beiträgen dieser Serie angewendeten Herangehensweise vorbereitet. Also sind alle Funktionen in verschiedenen Dateien sortiert und in der Hauptdatei des Projekts enthalten. Sie sehen, wie Dateien in das Projekt eingebunden werden können, im Beitrag "Das MQL5-Kochbuch: Verwendung von Indikatoren zum Festlegen von Handelsbedingungen in Expert Advisors".

Um während der Optimierung Zugriff auf die Daten zu erhalten, können Sie spezielle MQL5-Funktionen nutzen: OnTesterInit(), OnTester(), OnTesterPass() und OnTesterDeinit(). Sehen wir uns jede von ihnen kurz an:

  • OnTesterInit() – diese Funktion wird zum Bestimmen des Startpunkts der Optimierung verwendet.
  • OnTester() – diese Funktion fügt nach jedem Optimierungsdurchlauf sogenannte Frames hinzu. Die Definition von Frames folgt im weiteren Verlauf des Beitrags.
  • OnTesterPass() – diese Funktion empfängt Frames nach jedem Optimierungsdurchlauf.
  • OnTesterDeinit() – diese Funktion erzeugt das Ereignis des Endes der Optimierung der Parameter des Expert Advisors.

Nun müssen wir definieren, was ein Frame ist. Ein Frame ist eine Art Datenstruktur eines einzelnen Optimierungsdurchlaufs. Während der Optimierung werden Frames im *.mqd-Archiv im Ordner MetaTrader 5\MQL5\Files\Tester gespeichert. Der Zugriff auf die Daten (Frames) dieses Archivs ist sowohl spontan während der Optimierung als auch nach der Optimierung möglich. Beispielsweise illustriert der Beitrag "Veranschaulichung einer Strategie im Prüfprogramm von MetaTrader 5", wie wir den Prozess der Optimierung ohne Vorbereitung visualisieren und anschließend das Ergebnis der Optimierung betrachten können.

In diesem Beitrag nutzen wir die folgenden Funktionen für die Arbeit mit Frames:

  • FrameAdd() – fügt Daten aus einer Datei oder einem Array hinzu.
  • FrameNext() – Aufruf zum Erhalten eines einzelnen numerischen Werts oder der gesamten Daten des Frames.
  • FrameInputs() – ruft Eingabeparameter ab, auf deren Basis ein bestimmter Frame mit der angegebenen Durchlaufnummer geformt wird.

Weitere Informationen über die oben aufgeführten Funktionen finden Sie im Nachschlagewerk MQL5. Wir fangen wie immer mit den externen Parametern an. Unten sehen Sie, welche Parameter zu den bereits vorhandenen hinzugefügt werden müssen:

//--- External parameters of the Expert Advisor
input  int              NumberOfBars           = 2;              // Number of one-direction bars
sinput double           Lot                    = 0.1;            // Lot
input  double           TakeProfit             = 100;            // Take Profit
input  double           StopLoss               = 50;             // Stop Loss
input  double           TrailingStop           = 10;             // Trailing Stop
input  bool             Reverse                = true;           // Position reversal
sinput string           delimeter=""; // --------------------------------
sinput bool             LogOptimizationReport  = true;           // Writing results to a file
sinput CRITERION_RULE   CriterionSelectionRule = RULE_AND;       // Condition for writing
sinput ENUM_STATS       Criterion_01           = C_NO_CRITERION; // 01 - Criterion name
sinput double           CriterionValue_01      = 0;              // ---- Criterion value
sinput ENUM_STATS       Criterion_02           = C_NO_CRITERION; // 02 - Criterion name
sinput double           CriterionValue_02      = 0;              // ---- Criterion value
sinput ENUM_STATS       Criterion_03           = C_NO_CRITERION; // 03 - Criterion name
sinput double           CriterionValue_03      = 0;              // ---- Criterion value

Der Parameter LogOptimizationReport zeigt, ob die Ergebnisse und Parameter während der Optimierung in eine Datei geschrieben werden sollten.

In diesem Beispiel implementieren wir eine Möglichkeit, bis zu drei Kriterien anzugeben, anhand derer die in eine Datei zu schreibenden Ergebnisse ausgewählt werden. Außerdem fügen wir eine Regel hinzu (Parameter CriterionSelectionRule), in der Sie festlegen können, ob die Ergebnisse geschrieben werden, wenn entweder alle angegebenen Bedingungen erfüllt sind (UND) oder mindestens eine von ihnen (ODER). Zu diesem Zweck erstellen wir eine Aufzählung in der Datei Enums.mqh:

//--- Rules for checking criteria
enum CRITERION_RULE
  {
   RULE_AND = 0, // AND
   RULE_OR  = 1  // OR
  };

Die Hauptparameter des Tests werden als Kriterien verwendet. Hier benötigen wir eine weitere Aufzählung:

//--- Statistical parameters
enum ENUM_STATS
  {
   C_NO_CRITERION              = 0, // No criterion
   C_STAT_PROFIT               = 1, // Profit
   C_STAT_DEALS                = 2, // Total deals
   C_STAT_PROFIT_FACTOR        = 3, // Profit factor
   C_STAT_EXPECTED_PAYOFF      = 4, // Expected payoff
   C_STAT_EQUITY_DDREL_PERCENT = 5, // Max. equity drawdown, %
   C_STAT_RECOVERY_FACTOR      = 6, // Recovery factor
   C_STAT_SHARPE_RATIO         = 7  // Sharpe ratio
  };

Jeder Parameter wird auf die Überschreitung des in den externen Parameters angegebenen Werts geprüft. Davon ausgenommen ist der maximale Wertverlust des Eigenkapitals, da die Auswahl auf Basis des minimalen Wertverlustes getroffen werden muss.

Wir müssen auch einige globale Variablen hinzufügen (siehe nachfolgender Code):

//--- Global variables
int    AllowedNumberOfBars=0;       // For checking the NumberOfBars external parameter value
string OptimizationResultsPath=""// Location of the folder for saving folders and files
int    UsedCriteriaCount=0;         // Number of used criteria
int    OptimizationFileHandle=-1;   // Handle of the file of optimization results

Außerdem werden die folgenden Arrays benötigt:

int    criteria[3];                    // Criteria for optimization report generation
double criteria_values[3];             // Values of criteria
double stat_values[STAT_VALUES_COUNT]; // Array for testing parameters

Die Hauptdatei des Expert Advisors muss um Funktionen für die Verarbeitung von Ereignissen des Strategietesters erweitert werden, die am Anfang dieses Beitrags beschrieben wurden:

//+--------------------------------------------------------------------+
//| Optimization start                                                 |
//+--------------------------------------------------------------------+
void OnTesterInit()
  {
   Print(__FUNCTION__,"(): Start Optimization \n-----------");
  }
//+--------------------------------------------------------------------+
//| Test completion event handler                                      |
//+--------------------------------------------------------------------+
double OnTester()
  {
//--- If writing of optimization results is enabled
   if(LogOptimizationReport)
      //---
//---
   return(0.0);
  }
//+--------------------------------------------------------------------+
//| Next optimization pass                                             |
//+--------------------------------------------------------------------+
void OnTesterPass()
  {
//--- If writing of optimization results is enabled
   if(LogOptimizationReport)
      //---
  }
//+--------------------------------------------------------------------+
//| End of optimization                                                |
//+--------------------------------------------------------------------+
void OnTesterDeinit()
  {
   Print("-----------\n",__FUNCTION__,"(): End Optimization");
//--- If writing of optimization results is enabled
   if(LogOptimizationReport)
      //---
  }

Wenn wir die Optimierung jetzt starten, erscheint das Diagramm mit dem Symbol und Timeframe, auf denen der Expert Advisor ausgeführt wird, im Terminal. Nachrichten aus den im oben aufgeführten Code verwendeten Funktionen werden im Logbuch des Terminals anstelle des Logbuchs des Strategietesters gedruckt. Eine Nachricht der Funktion OnTesterInit() wird gleich am Anfang der Optimierung gedruckt. Während der Optimierung und nach ihrem Abschluss werden Sie allerdings keine Nachrichten im Logbuch sehen können. Wenn Sie das durch den Strategietester geöffnete Diagramm nach der Optimierung löschen, wird eine Nachricht der Funktion OnTesterDeinit() im Logbuch gedruckt. Warum?

Das Problem ist, dass die Funktion OnTester() die Funktion FrameAdd() nutzen muss, um einen Frame hinzuzufügen, wie unten abgebildet, um einen korrekten Betrieb sicherzustellen.

//+--------------------------------------------------------------------+
//| Test completion event handler                                      |
//+--------------------------------------------------------------------+
double OnTester()
  {
//--- If writing of optimization results is enabled
   if(LogOptimizationReport)
     {
      //--- Create a frame
      FrameAdd("Statistics",1,0,stat_values);
     }
//---
   return(0.0);
  }

Jetzt wird während der Optimierung eine Nachricht der Funktion OnTesterPass() nach jedem Optimierungsdurchlauf in das Logbuch gedruckt und die Nachricht über den Abschluss der Optimierung wird am Ende der Optimierung durch die Funktion OnTesterDeinit() hinzugefügt. Die Nachricht über den Abschluss der Optimierung wird auch dann erzeugt, wenn die Optimierung manuell angehalten wird.

Abb. 1 – Ins Logbuch gedruckte Nachrichten aus Test- und Optimierungsfunktionen

Abb. 1 – Ins Logbuch gedruckte Nachrichten aus Test- und Optimierungsfunktionen

Nun ist alles bereit und wir können mit den Funktionen zum Erstellen von Ordnern und Dateien, der Bestimmung festgelegter Optimierungsparameter und dem Schreiben der Ergebnisse, die die Bedingungen erfüllen, fortfahren.

Erstellen wir die Datei FileFunctions.mqh und fügen sie ins Projekt ein. Am Anfang der Datei schreiben wir die Funktion GetTestStatistics(), die per Verweis ein Array zum Ausfüllen jedes Optimierungsdurchlaufs mit Werten erhält.

//+--------------------------------------------------------------------+
//| Filling the array with test results                                |
//+--------------------------------------------------------------------+
void GetTestStatistics(double &stat_array[])
  {
//--- Auxiliary variables for value adjustment
   double profit_factor=0,sharpe_ratio=0;
//---
   stat_array[0]=TesterStatistics(STAT_PROFIT);                // Net profit upon completion of testing
   stat_array[1]=TesterStatistics(STAT_DEALS);                 // Number of executed deals
//---
   profit_factor=TesterStatistics(STAT_PROFIT_FACTOR);         // Profit factor – the STAT_GROSS_PROFIT/STAT_GROSS_LOSS ratio 
   stat_array[2]=(profit_factor==DBL_MAX) ? 0 : profit_factor; // adjust if necessary
//---
   stat_array[3]=TesterStatistics(STAT_EXPECTED_PAYOFF);       // Expected payoff
   stat_array[4]=TesterStatistics(STAT_EQUITY_DDREL_PERCENT);  // Max. equity drawdown, %
   stat_array[5]=TesterStatistics(STAT_RECOVERY_FACTOR);       // Recovery factor – the STAT_PROFIT/STAT_BALANCE_DD ratio
//---
   sharpe_ratio=TesterStatistics(STAT_SHARPE_RATIO);           // Sharpe ratio - investment portfolio (asset) efficiency index
   stat_array[6]=(sharpe_ratio==DBL_MAX) ? 0 : sharpe_ratio;   // adjust if necessary
  }

Die Funktion GetTestStatistics() muss vor der Einfügung eines Frames eingefügt werden:

//+--------------------------------------------------------------------+
//| Test completion event handler                                      |
//+--------------------------------------------------------------------+
double OnTester()
  {
//--- If writing of optimization results is enabled
   if(LogOptimizationReport)
     {
      //--- Fill the array with test values
      GetTestStatistics(stat_values);
      //--- Create a frame
      FrameAdd("Statistics",1,0,stat_values);
     }
//---
   return(0.0);
  }

Das ausgefüllte Array wird als letztes Argument an die Funktion FrameAdd() übergeben. Falls erforderlich, können Sie sogar eine Datendatei übergeben.

In der Funktion OnTesterPass() können wir die erhaltenen Daten nun prüfen. Um zu sehen, wie das funktioniert, zeigen wir zunächst einfach den Gewinn aus jedem Ergebnis im Logbuch des Terminals an. Nutzen Sie die Funktion FrameNext(), um die Werte des aktuellen Frames zu erhalten. Siehe folgendes Beispiel:

//+--------------------------------------------------------------------+
//| Next optimization pass                                             |
//+--------------------------------------------------------------------+
void OnTesterPass()
  {
//--- If writing of optimization results is enabled
   if(LogOptimizationReport)
     {
      string name ="";  // Public name/frame label
      ulong  pass =0;   // Number of the optimization pass at which the frame is added
      long   id   =0;   // Public id of the frame
      double val  =0.0; // Single numerical value of the frame
      //---
      FrameNext(pass,name,id,val,stat_values);
      //---
      Print(__FUNCTION__,"(): pass: "+IntegerToString(pass)+"; STAT_PROFIT: ",DoubleToString(stat_values[0],2));
     }
  }

Wenn Sie die Funktion FrameNext() nicht nutzen, sind die Werte im Array stat_values gleich Null. Wenn alles richtig gemacht wird, erhalten wir das Ergebnis, das im nachfolgenden Screenshot abgebildet ist:

Abb. 2 – Ins Logbuch gedruckte Nachrichten der Funktion OnTesterPass()

Abb. 2 – Ins Logbuch gedruckte Nachrichten der Funktion OnTesterPass()

Übrigens, wenn die Optimierung durchgeführt wird, ohne die externen Parameter zu modifizieren, werden die Ergebnisse aus dem Zwischenspeicher in den Strategietester geladen und umgehen die Funktionen OnTesterPass() und OnTesterDeinit(). Denken Sie daran, um nicht zu dem Trugschluss zu kommen, dass es einen Fehler gibt.

In der Datei FileFunctions.mqh erstellen wir die Funktion CreateOptimizationReport(). Die wichtigsten Aktivitäten finden in dieser Funktion statt. Der Code der Funktion ist nachfolgend aufgeführt:

//+--------------------------------------------------------------------+
//| Generating and writing report on optimization results              |
//+--------------------------------------------------------------------+
void CreateOptimizationReport()
  {
   static int passes_count=0;                // Pass counter
   int        parameters_count=0;            // Number of Expert Advisor parameters
   int        optimized_parameters_count=0;  // Counter of optimized parameters
   string     string_to_write="";            // String for writing
   bool       include_criteria_list=false;   // For determining the start of the list of parameters/criteria
   int        equality_sign_index=0;         // The '=' sign index in the string
   string     name            ="";           // Public name/frame label
   ulong      pass            =0;            // Number of the optimization pass at which the frame is added
   long       id              =0;            // Public id of the frame
   double     value           =0.0;          // Single numerical value of the frame
   string     parameters_list[];             // List of the Expert Advisor parameters of the "parameterN=valueN" form
   string     parameter_names[];             // Array of parameter names
   string     parameter_values[];            // Array of parameter values
//--- Increase the pass counter
   passes_count++;
//--- Place statistical values into the array
   FrameNext(pass,name,id,value,stat_values);
//--- Get the pass number, list of parameters, number of parameters
   FrameInputs(pass,parameters_list,parameters_count);
//--- Iterate over the list of parameters in a loop (starting from the upper one on the list)
//    The list starts with the parameters that are flagged for optimization
   for(int i=0; i<parameters_count; i++)
     {
      //--- Get the criteria for selection of results at the first pass
      if(passes_count==1)
        {
         string current_value="";      // Current parameter value
         static int c=0,v=0,trigger=0; // Counters and trigger
         //--- Set a flag if you reached the list of criteria
         if(StringFind(parameters_list[i],"CriterionSelectionRule",0)>=0)
           {
            include_criteria_list=true;
            continue;
           }
         //--- At the last parameter, count the used criteria,
         //    if the AND mode is selected
         if(CriterionSelectionRule==RULE_AND && i==parameters_count-1)
            CalculateUsedCriteria();
         //--- If you reached criteria in the parameter list
         if(include_criteria_list)
           {
            //--- Determine names of criteria
            if(trigger==0)
              {
               equality_sign_index=StringFind(parameters_list[i],"=",0)+1;          // Determine the '=' sign position in the string
               current_value =StringSubstr(parameters_list[i],equality_sign_index); // Get the parameter value
               //---
               criteria[c]=(int)StringToInteger(current_value);
               trigger=1; // Next parameter will be a value
               c++;
               continue;
              }
            //--- Determine values of criteria
            if(trigger==1)
              {
               equality_sign_index=StringFind(parameters_list[i],"=",0)+1;          // Determine the '=' sign position in the string
               current_value=StringSubstr(parameters_list[i],equality_sign_index);  // Get the parameter value
               //---           
               criteria_values[v]=StringToDouble(current_value);
               trigger=0; // Next parameter will be a criterion
               v++; 
               continue;
              }
           }
        }
      //--- If the parameter is enabled for optimization
      if(ParameterEnabledForOptimization(parameters_list[i]))
        {
         //--- Increase the counter of the optimized parameters
         optimized_parameters_count++;
         //--- Write the names of the optimized parameters to the array
         //    only at the first pass (for headers)
         if(passes_count==1)
           {
            //--- Increase the size of the array of parameter values
            ArrayResize(parameter_names,optimized_parameters_count);
            //--- Determine the '=' sign position         
            equality_sign_index=StringFind(parameters_list[i],"=",0);
            //--- Take the parameter name
            parameter_names[i]=StringSubstr(parameters_list[i],0,equality_sign_index);
           }
         //--- Increase the size of the array of parameter values
         ArrayResize(parameter_values,optimized_parameters_count);
         //--- Determine the '=' sign position
         equality_sign_index=StringFind(parameters_list[i],"=",0)+1;
         //--- Take the parameter value
         parameter_values[i]=StringSubstr(parameters_list[i],equality_sign_index);
        }
     }
//--- Generate a string of values to the optimized parameters
   for(int i=0; i<STAT_VALUES_COUNT; i++)
      StringAdd(string_to_write,DoubleToString(stat_values[i],2)+",");
//--- Add values of the optimized parameters to the string of values
   for(int i=0; i<optimized_parameters_count; i++)
     {
      //--- If it is the last value in the string, do not use the separator
      if(i==optimized_parameters_count-1)
        {
         StringAdd(string_to_write,parameter_values[i]);
         break;
        }
      //--- Otherwise use the separator
      else
         StringAdd(string_to_write,parameter_values[i]+",");
     }
//--- At the first pass, generate the optimization report file with headers
   if(passes_count==1)
      WriteOptimizationReport(parameter_names);
//--- Write data to the file of optimization results
   WriteOptimizationResults(string_to_write);
  }

Wir haben eine ziemlich große Funktion erhalten. Sehen wir sie uns näher an. Am Anfang, gleich nach der Deklarierung der Variablen und Arrays, erhalten wir mithilfe der Funktion FrameNext() die Daten des Frames, wie es in den weiter oben aufgeführten Beispielen demonstriert wird. Dann erhalten wir mithilfe der Funktion FrameInputs() die Liste der Parameter im String-Array parameters_list[] zusammen mit der Gesamtmenge der Parameter, die an die Variable parameters_count übergeben wird.

Die optimierten Parameter (im Strategietester mit Flags gekennzeichnet) in der Parameterliste aus der Funktion FrameInputs() befinden sich gleich am Anfang, unabhängig von ihrer Reihenfolge in der Liste der externen Parameter des Expert Advisors.

Darauf folgt eine Schleife, die über die Parameterliste iteriert. Das Kriterien-Array criteria[] und das Array der Werte von Kriterien criteria_values[] werden beim ersten Durchlauf befüllt. Die verwendeten Kriterien werden in der Funktion CalculateUsedCriteria() gezählt, wenn der Modus AND aktiv ist und der aktuelle Parameter der letzte ist:

//+--------------------------------------------------------------------+
//| Counting the number of used criteria                               |
//+--------------------------------------------------------------------+
void CalculateUsedCriteria()
  {
   UsedCriteriaCount=0; // Zeroing out
//--- Iterate over the list of criteria in a loop
   for(int i=0; i<ArraySize(criteria); i++)
     {
      //--- count the used criteria
      if(criteria[i]!=C_NO_CRITERION)
         UsedCriteriaCount++;
     }
  }

In derselben Schleife prüfen wir im weiteren Verlauf, ob irgendein Parameter für die Optimierung ausgewählt ist. Diese Prüfung wird bei jedem Durchlauf durchgeführt und wird durch die Funktion ParameterEnabledForOptimization() ausgelöst, an die der aktuelle externe Parameter zur Prüfung übergeben wird. Wenn die Funktion true ausgibt, wird der Parameter optimiert.

//+---------------------------------------------------------------------+
//| Checking whether the external parameter is enabled for optimization |
//+---------------------------------------------------------------------+
bool ParameterEnabledForOptimization(string parameter_string)
  {
   bool enable;
   long value,start,step,stop;
//--- Determine the '=' sign position in the string
   int equality_sign_index=StringFind(parameter_string,"=",0);
//--- Get the parameter values
   ParameterGetRange(StringSubstr(parameter_string,0,equality_sign_index),
                     enable,value,start,step,stop);
//--- Return the parameter status
   return(enable);
  }

In diesem Fall werden die Arrays für Namen (parameter_names) und Parameterwerte (parameter_values) ausgefüllt. Das Array für Namen optimierter Parameter wird nur beim ersten Durchlauf befüllt.

Anschließend erzeugen wir mithilfe zweier Schleifen den String von Test- und Parameterwerten zum Schreiben in eine Datei. Dann wird die Datei zum Schreiben mithilfe der Funktion WriteOptimizationReport() beim ersten Durchlauf erzeugt.

//+--------------------------------------------------------------------+
//| Generating the optimization report file                            |
//+--------------------------------------------------------------------+
void WriteOptimizationReport(string &parameter_names[])
  {
   int files_count     =1; // Counter of optimization files
//--- Generate a header to the optimized parameters
   string headers="#,PROFIT,TOTAL DEALS,PROFIT FACTOR,EXPECTED PAYOFF,EQUITY DD MAX REL%,RECOVERY FACTOR,SHARPE RATIO,";
//--- Add the optimized parameters to the header
   for(int i=0; i<ArraySize(parameter_names); i++)
     {
      if(i==ArraySize(parameter_names)-1)
         StringAdd(headers,parameter_names[i]);
      else
         StringAdd(headers,parameter_names[i]+",");
     }
//--- Get the location for the optimization file and the number of files for the index number
   OptimizationResultsPath=CreateOptimizationResultsFolder(files_count);
//--- If there is an error when getting the folder, exit
   if(OptimizationResultsPath=="")
     {
      Print("Empty path: ",OptimizationResultsPath);
      return;
     }
   else
     {
      OptimizationFileHandle=FileOpen(OptimizationResultsPath+"\optimization_results"+IntegerToString(files_count)+".csv",
                                      FILE_CSV|FILE_READ|FILE_WRITE|FILE_ANSI|FILE_COMMON,",");
      //---
      if(OptimizationFileHandle!=INVALID_HANDLE)
         FileWrite(OptimizationFileHandle,headers);
     }
  }

Der Zweck der Funktion WriteOptimizationReport() ist die Erzeugung von Kopfzeilen, die bedarfsgemäße Erstellung von Ordnern im Stammverzeichnis des Terminals sowie die Erstellung einer Datei zum Schreiben. Das heißt, Dateien, die mit vorhergehenden Optimierungen zusammenhängen, werden nicht entfernt und die Funktion erstellt jedes Mal eine neue Datei mit der Indexnummer. Kopfzeilen werden in einer neu erstellen Datei gespeichert. Die Datei selbst bleibt bis zum Ende der Optimierung geöffnet.

Der oben aufgeführte Code enthält den String mit der Funktion CreateOptimizationResultsFolder(), in der Ordner zum Speichern von Dateien mit Optimierungsergebnissen gespeichert werden:

//+--------------------------------------------------------------------+
//| Creating folders for optimization results                          |
//+--------------------------------------------------------------------+
string CreateOptimizationResultsFolder(int &files_count)
  {
   long   search_handle       =INVALID_HANDLE;        // Search handle
   string returned_filename   ="";                    // Name of the found object (file/folder)
   string path                ="";                    // File/folder search location
   string search_filter       ="*";                   // Search filter (* - check all files/folders)
   string root_folder         ="OPTIMIZATION_DATA\\"; // Root folder
   string expert_folder       =EXPERT_NAME+"\\";      // Folder of the Expert Advisor
   bool   root_folder_exists  =false;                 // Flag of existence of the root folder
   bool   expert_folder_exists=false;                 // Flag of existence of the Expert Advisor folder
//--- Search for the OPTIMIZATION_DATA root folder in the common folder of the terminal
   path=search_filter;
//--- Set the search handle in the common folder of all client terminals \Files
   search_handle=FileFindFirst(path,returned_filename,FILE_COMMON);
//--- Print the location of the common folder of the terminal to the journal
   Print("TERMINAL_COMMONDATA_PATH: ",COMMONDATA_PATH);
//--- If the first folder is the root folder, flag it
   if(returned_filename==root_folder)
     {
      root_folder_exists=true;
      Print("The "+root_folder+" root folder exists.");
     }
//--- If the search handle has been obtained
   if(search_handle!=INVALID_HANDLE)
     {
      //--- If the first folder is not the root folder
      if(!root_folder_exists)
        {
         //--- Iterate over all files to find the root folder
         while(FileFindNext(search_handle,returned_filename))
           {
            //--- If it is found, flag it
            if(returned_filename==root_folder)
              {
               root_folder_exists=true;
               Print("The "+root_folder+" root folder exists.");
               break;
              }
           }
        }
      //--- Close the root folder search handle
      FileFindClose(search_handle);
     }
   else
     {
      Print("Error when getting the search handle "
            "or the "+COMMONDATA_PATH+" folder is empty: ",ErrorDescription(GetLastError()));
     }
//--- Search for the Expert Advisor folder in the OPTIMIZATION_DATA folder
   path=root_folder+search_filter;
//--- Set the search handle in the ..\Files\OPTIMIZATION_DATA\ folder
   search_handle=FileFindFirst(path,returned_filename,FILE_COMMON);
//--- If the first folder is the folder of the Expert Advisor
   if(returned_filename==expert_folder)
     {
      expert_folder_exists=true; // Remember this
      Print("The "+expert_folder+" Expert Advisor folder exists.");
     }
//--- If the search handle has been obtained
   if(search_handle!=INVALID_HANDLE)
     {
      //--- If the first folder is not the folder of the Expert Advisor
      if(!expert_folder_exists)
        {
         //--- Iterate over all files in the DATA_OPTIMIZATION folder to find the folder of the Expert Advisor
         while(FileFindNext(search_handle,returned_filename))
           {
            //--- If it is found, flag it
            if(returned_filename==expert_folder)
              {
               expert_folder_exists=true;
               Print("The "+expert_folder+" Expert Advisor folder exists.");
               break;
              }
           }
        }
      //--- Close the root folder search handle
      FileFindClose(search_handle);
     }
   else
      Print("Error when getting the search handle or the "+path+" folder is empty.");
//--- Generate the path to count the files
   path=root_folder+expert_folder+search_filter;
//--- Set the search handle in the ..\Files\OPTIMIZATION_DATA\ folder of optimization results
   search_handle=FileFindFirst(path,returned_filename,FILE_COMMON);
//--- If the folder is not empty, start the count
   if(StringFind(returned_filename,"optimization_results",0)>=0)
      files_count++;
//--- If the search handle has been obtained
   if(search_handle!=INVALID_HANDLE)
     {
      //--- Count all files in the Expert Advisor folder
      while(FileFindNext(search_handle,returned_filename))
         files_count++;
      //---
      Print("Total files: ",files_count);
      //--- Close the Expert Advisor folder search handle
      FileFindClose(search_handle);
     }
   else
      Print("Error when getting the search handle or the "+path+" folder is empty");
//--- Create the necessary folders based on the check results
//    If there is no OPTIMIZATION_DATA root folder
   if(!root_folder_exists)
     {
      if(FolderCreate("OPTIMIZATION_DATA",FILE_COMMON))
        {
         root_folder_exists=true;
         Print("The root folder ..\Files\OPTIMIZATION_DATA\\ has been created");
        }
      else
        {
         Print("Error when creating the OPTIMIZATION_DATA root folder: ",
               ErrorDescription(GetLastError()));
         return("");
        }
     }
//--- If there is no Expert Advisor folder
   if(!expert_folder_exists)
     {
      if(FolderCreate(root_folder+EXPERT_NAME,FILE_COMMON))
        {
         expert_folder_exists=true;
         Print("The Expert Advisor folder ..\Files\OPTIMIZATION_DATA\\ has been created"+expert_folder);
        }
      else
        {
         Print("Error when creating the Expert Advisor folder ..\Files\\"+expert_folder+"\: ",
               ErrorDescription(GetLastError()));
         return("");
        }
     }
//--- If the necessary folders exist
   if(root_folder_exists && expert_folder_exists)
     {
      //--- Return the location for creating the file of optimization results
      return(root_folder+EXPERT_NAME);
     }
//---
   return("");
  }

Der oben aufgeführte Code beinhaltet detaillierte Kommentare, sodass Sie keine Probleme haben sollten, ihn zu verstehen. Gehen wir also nur auf das Wichtigste ein.

Als Erstes prüfen wir das Vorhandensein des Stammverzeichnisses OPTIMIZATION_DATA, das die Ergebnisse der Optimierung beinhaltet. Falls der Ordner existiert, wird dies in der Variable root_folder_exists vermerkt. Das Such-Handle wird anschließend im Ordner OPTIMIZATION_DATA festgelegt, in dem wir das Vorhandensein des Ordners des Expert Advisors prüfen.

Weiter zählen wir die Dateien, die der Ordner des Expert Advisors enthält. Zuletzt werden auf Basis der Prüfergebnisse, falls erforderlich (wenn die Ordner nicht gefunden werden konnten), die erforderlichen Ordner erstellt und der Speicherort der neuen Datei mit der Indexnummer ausgegeben. Falls ein Fehler aufgetreten ist, wird ein leerer String ausgegeben.

Nun müssen wir nur die Funktion WriteOptimizationResults() betrachten, in der wir die Bedingungen für das Schreiben von Daten in die Datei prüfen und die Daten schreiben, wenn die Bedingungen erfüllt sind. Der Code dieser Funktion ist nachfolgend aufgeführt:

//+--------------------------------------------------------------------+
//| Writing the results of the optimization by criteria                |
//+--------------------------------------------------------------------+
void WriteOptimizationResults(string string_to_write)
  {
   bool condition=false; // To check the condition
//--- If at least one criterion is satisfied
   if(CriterionSelectionRule==RULE_OR)
      condition=AccessCriterionOR();
//--- If all criteria are satisfied
   if(CriterionSelectionRule==RULE_AND)
      condition=AccessCriterionAND();
//--- If the conditions for criteria are satisfied
   if(condition)
     {
      //--- If the file of optimization results is opened
      if(OptimizationFileHandle!=INVALID_HANDLE)
        {
         int strings_count=0; // String counter
         //--- Get the number of strings in the file and move the pointer to the end
         strings_count=GetStringsCount();
         //--- Write the string with criteria
         FileWrite(OptimizationFileHandle,IntegerToString(strings_count),string_to_write);
        }
      else
         Print("Invalid optimization file handle!");
     }
  }

Sehen wir uns die Strings an, die die im Code markierten Funktionen enthalten. Die Wahl der zu verwendenden Funktion hängt von der Regel ab, die für die Überprüfung der Kriterien ausgewählt wurde. Wenn alle angegebenen Kriterien erfüllt werden müssen, verwenden wir die Funktion AccessCriterionAND():

//+--------------------------------------------------------------------+
//| Checking multiple conditions for writing to the file               |
//+--------------------------------------------------------------------+
bool AccessCriterionAND()
  {
   int count=0; // Criterion counter
//--- Iterate over the array of criteria in a loop and see
//    if all the conditions for writing parameters to the file are met
   for(int i=0; i<ArraySize(criteria); i++)
     {
      //--- Move to the next iteration, if the criterion is not determined
      if(criteria[i]==C_NO_CRITERION)
         continue;
      //--- PROFIT
      if(criteria[i]==C_STAT_PROFIT)
        {
         if(stat_values[0]>criteria_values[i])
           {
            count++;
            if(count==UsedCriteriaCount)
               return(true);
           }
        }
      //--- TOTAL DEALS
      if(criteria[i]==C_STAT_DEALS)
        {
         if(stat_values[1]>criteria_values[i])
           {
            count++;
            if(count==UsedCriteriaCount)
               return(true);
           }
        }
      //--- PROFIT FACTOR
      if(criteria[i]==C_STAT_PROFIT_FACTOR)
        {
         if(stat_values[2]>criteria_values[i])
           {
            count++;
            if(count==UsedCriteriaCount)
               return(true);
           }
        }
      //--- EXPECTED PAYOFF
      if(criteria[i]==C_STAT_EXPECTED_PAYOFF)
        {
         if(stat_values[3]>criteria_values[i])
           {
            count++;
            if(count==UsedCriteriaCount)
               return(true);
           }
        }
      //--- EQUITY DD REL PERC
      if(criteria[i]==C_STAT_EQUITY_DDREL_PERCENT)
        {
         if(stat_values[4]<criteria_values[i])
           {
            count++;
            if(count==UsedCriteriaCount)
               return(true);
           }
        }
      //--- RECOVERY FACTOR
      if(criteria[i]==C_STAT_RECOVERY_FACTOR)
        {
         if(stat_values[5]>criteria_values[i])
           {
            count++;
            if(count==UsedCriteriaCount)
               return(true);
           }
        }
      //--- SHARPE RATIO
      if(criteria[i]==C_STAT_SHARPE_RATIO)
        {
         if(stat_values[6]>criteria_values[i])
           {
            count++;
            if(count==UsedCriteriaCount)
               return(true);
           }
        }
     }
//--- Conditions for writing are not met
   return(false);
  }

Wenn mindestens eins der angegebenen Kriterien erfüllt werden muss, nutzen Sie die Funktion AccessCriterionOR():

//+--------------------------------------------------------------------+
//| Checking for meeting one of the conditions for writing to the file |
//+--------------------------------------------------------------------+
bool AccessCriterionOR()
  {
//--- Iterate over the array of criteria in a loop and see 
//    if all the conditions for writing parameters to the file are met
   for(int i=0; i<ArraySize(criteria); i++)
     {
     //---
      if(criteria[i]==C_NO_CRITERION)
         continue;
      //--- PROFIT
      if(criteria[i]==C_STAT_PROFIT)
        {
         if(stat_values[0]>criteria_values[i])
            return(true);
        }
      //--- TOTAL DEALS
      if(criteria[i]==C_STAT_DEALS)
        {
         if(stat_values[1]>criteria_values[i])
            return(true);
        }
      //--- PROFIT FACTOR
      if(criteria[i]==C_STAT_PROFIT_FACTOR)
        {
         if(stat_values[2]>criteria_values[i])
            return(true);
        }
      //--- EXPECTED PAYOFF
      if(criteria[i]==C_STAT_EXPECTED_PAYOFF)
        {
         if(stat_values[3]>criteria_values[i])
            return(true);
        }
      //--- EQUITY DD REL PERC
      if(criteria[i]==C_STAT_EQUITY_DDREL_PERCENT)
        {
         if(stat_values[4]<criteria_values[i])
            return(true);
        }
      //--- RECOVERY FACTOR
      if(criteria[i]==C_STAT_RECOVERY_FACTOR)
        {
         if(stat_values[5]>criteria_values[i])
            return(true);
        }
      //--- SHARPE RATIO
      if(criteria[i]==C_STAT_SHARPE_RATIO)
        {
         if(stat_values[6]>criteria_values[i])
            return(true);
        }
     }
//--- Conditions for writing are not met
   return(false);
  }

Die Funktion GetStringsCount() verschiebt den Pointer zum Ende der Datei und gibt die Anzahl der Strings in der Datei aus:

//+--------------------------------------------------------------------+
//| Counting the number of strings in the file                         |
//+--------------------------------------------------------------------+
int GetStringsCount()
  {
   int   strings_count =0;  // String counter
   ulong offset        =0;  // Offset for determining the position of the file pointer
//--- Move the file pointer to the beginning
   FileSeek(OptimizationFileHandle,0,SEEK_SET);
//--- Read until the current position of the file pointer reaches the end of the file
   while(!FileIsEnding(OptimizationFileHandle) || !IsStopped())
     {
      //--- Read the whole string
      while(!FileIsLineEnding(OptimizationFileHandle) || !IsStopped())
        {
         //--- Read the string
         FileReadString(OptimizationFileHandle);
         //--- Get the position of the pointer
         offset=FileTell(OptimizationFileHandle);
         //--- If it's the end of the string
         if(FileIsLineEnding(OptimizationFileHandle))
           {
            //--- Move to the next string 
            //    if it's not the end of the file, increase the pointer counter
            if(!FileIsEnding(OptimizationFileHandle))
               offset++;
            //--- Move the pointer
            FileSeek(OptimizationFileHandle,offset,SEEK_SET);
            //--- Increase the string counter
            strings_count++;
            break;
           }
        }
      //--- If it's the end of the file, exit the loop
      if(FileIsEnding(OptimizationFileHandle))
         break;
     }
//--- Move the pointer to the end of the file for writing
   FileSeek(OptimizationFileHandle,0,SEEK_END);
//--- Return the number of strings
   return(strings_count);
  }

Nun ist alles fertig. Jetzt müssen wir die Funktion CreateOptimizationReport() in den Körper der Funktion OnTesterPass() einfügen und das Handle der Datei mit den Optimierungsergebnissen in der Funktion OnTesterDeinit() schließen.

Testen wir nun den Expert Advisor. Seine Parameter werden mithilfe des MQL5 Cloud Network optimiert. Der Strategietester muss gemäß dem folgenden Screenshot eingestellt werden:

Abb. 3 – Einstellungen des Strategietesters

Abb. 3 – Einstellungen des Strategietesters

Wir optimieren alle Parameter des Expert Advisors und stellen die Parameter so ein, dass nur Ergebnisse, in denen Profit Factor größer als 1 und Recovery Factor größer als 2 sind, in die Datei geschrieben werden (siehe nachfolgender Screenshot):

Abb. 4 – Einstellungen des Expert Advisors für die Optimierung der Parameter

Abb. 4 – Einstellungen des Expert Advisors für die Optimierung der Parameter

Das MQL5 Cloud Network hat 101.000 Durchgänge in nur etwa 5 Minuten verarbeitet! Hätte ich nicht auf die Netzwerkressourcen zurückgegriffen, dann hätte die Optimierung mehrere Tage gedauert. Das ist eine großartige Gelegenheit für alle, die wissen, wie wertvoll Zeit ist.

Die erhaltene Datei kann nun in Excel geöffnet werden. Aus 101.000 Durchläufen wurden 719 Ergebnisse ausgewählt, die in die Datei geschrieben werden. Im nachfolgenden Screenshot habe ich die Spalten mit den Parametern markiert, auf deren Basis die Ergebnisse ausgewählt wurden:

Abb. 5 – Optimierungsergebnisse in Excel

Abb. 5 – Optimierungsergebnisse in Excel

 

Fazit

Damit ist dieser Beitrag abgeschlossen. Das Thema der Analyse von Optimierungsergebnissen kann noch viel weiter vertieft werden und wir werden in zukünftigen Beiträgen sicherlich darauf zurückkommen. Im Anhang des Beitrags finden Sie ein Archiv mit den Dateien des Expert Advisors.