Neuronales Netz: Selbstoptimierender Expert Advisor

Jose Miguel Soriano | 20 Oktober, 2016


Einleitung

Nachdem der Trader sich für eine Strategie entschieden und diese in einem EA implementiert hat, wird er mit zwei Problemen konfrontiert. Wenn diese Probleme nicht gelöst werden, machen sie alle Bemühungen zunichte.

  • Welche Werte sind als Inputparameter zu setzen?
  • Bis wann sind diese Werte zuverlässig? Wann ist die Optimierung erneut durchzuführen?
Im Gegensatz zu den Parametern, die im Voraus gesetzt werden (Währungspaar, Zeitrahmen usw.), gibt es auch andere, veränderbare Parameter wie Berechnungsperiode von Indikatoren, Sell und Buy Levels, TP/SL Levels u.a. Dies kann einige Probleme bei der Verwendung des Expert Advisors hervorrufen.

Ist es möglich, einen Expert Advisor zu erstellen, der Kriterien für das Eröffnen und Schließen von Positionen in vorgegebenen Abständen selbständig optimieren würde?

Was geschieht, wenn ein neuronales Netz (mehrschichtiges Perzeptron) als Modul für die Analyse der Historie und Bewertung der Strategie in dem EA implementiert wird? Kann man dem Code den Befehl geben, das Netz jeden Monat (jede Woche, jeden Tag oder jede Stunde) zu optimieren und anschließend die Arbeit fortzusetzen? Somit kommen wir zur Idee der Entwicklung eines selbstoptimierenden Expert Advisors.

Es ist nicht das erste Mal, dass das Thema "MQL und neuronale Netze" in der Trader Community angesprochen wird. Häufig läuft aber die Diskussion darauf hinaus, Informationen eines externen neuronalen Netzes zu verwenden (manchmal auch manuell) oder ein neuronales Netz mit dem МetaTrader4/МetaTrader5 Optimierer (im Modus "kontrolloses Training") zu optimieren. Im Endeffekt werden die Inputparameter des Expert Advisors durch die Inputparameter des Netzes ersetzt, wie z.B. in diesem Artikel.

Im vorliegenden Artikel werden wir keinen Handelsroboter beschreiben. Stattdessen werden wir das Modell eines Expert Advisors Modul für Modul entwickeln und implementieren, welcher mithilfe eines mehrschichtigen Perzeptrons (MSP, implementiert in mql5 mithilfe der ALGLIB Bibliothek) den oben erwähnten Algorithmus ausführen wird. Geben wir dem neuronalen Netz zwei mathematische Aufgaben auf, deren Lösungen sich anhand eines anderen Algorithmus leicht überprüfen lassen. So können wir die Lösungen und Aufgaben analysieren, die der mehrschichtige Perzeptron entsprechend seiner internen Struktur plant, und ein Kriterium für die Erstellung eines Expert Advisors erhalten, indem wir nur das Modul für die Dateneingabe modifizieren werden. Das zu erstellende Modell muss das Problem der Selbstoptimierung lösen.

Wir gehen davon aus, dass der Leser mit der allgemeinen Theorie der neuronalen Netze und solchen Begriffen wie Struktur und Organisationsformen, Anzahl von Schichten, Anzahl der Neuronen in jeder Schicht, Verbindungen, Gewichte usw. vertraut ist. Diese Informationen kann man auch in einschlägigen Artikeln finden.


1. Basisalgorithmus

  1. Erstellung eines neuronalen Netzes.
  2. Vorbereitung von Eingabedaten (und entsprechenden Ausgabedaten) durch das Laden der Daten ins Array.
  3. Normierung der Daten innerhalb eines bestimmten Bereichs (in der Regel [0, 1] oder [-1, 1]).
  4. Training und Optimierung des neuronalen Netzes.
  5. Berechnung und Verwendung der Prognose des Netzes entsprechend der Strategie des Expert Advisors.
  6. Selbstoptimierung: zurück zum Punkt 2 und eine erneute Iteration des Prozesses mit dem Hinzufügen des Prozesses in die OnTimer() Funktion.

Nach dem beschriebenen Algorithmus wird der Roboter eine regelmäßige Optimierung in den vom Nutzer vorgegebenen Abständen durchführen. Der Nutzer braucht sich keine Gedanken darüber zu machen. Der Schritt 5 ist nicht in den Reiterationsprozess eingeschlossen. Dem Expert Advisor werden immer Prognosewerte zur Verfügung stehen, auch während der Optimierung. Schauen wir mal, wie das funktioniert.

 

2. ALGLIB Bibliothek

Diese Bibliothek wurde in der Publikation von Sergej Bochkanov sowie auf der Webseite des ALGLIB Projekts veröffentlicht und diskutiert, http://www.alglib.net/, wo sie als eine Cross-Plattform Bibliothek für die numerische Datenanalyse bezeichnet wurde. Sie ist mit verschiedenen Programmiersprachen (C++, C#, Pascal, VBA) und Betriebssystemen (Windows, Linux, Solaris) kompatibel. Die ALGLIB Bibliothek verfügt über umfangreiche Funktionen. Die Bibliothek beinhaltet:

  • Lineare Algebra (direct algorithms, EVD/SVD)
  • Funktionen für das Lösen von Gleichungen (lineare und nichtlineare)
  • Interpolation
  • Optimierung
  • Fast Fourier Transformation
  • Numerische Integration
  • Lineare und nichtlineare kleinste Quadrate
  • Gewöhnliche Differentialgleichungen
  • Spezielle Funktionen
  • Statistik (deskriptive Statistik, Hypothesentest)
  • Datenanalyse (Klassifizierung / Regression, inkl. neuronale Netze)
  • Umsetzung von Algorithmen der linearen Algebra, Interpolation usw. hochgenaue Arithmetik (mit MPFR)

Für die Arbeit mit der Bibliothek werden statische Funktionen der CAlglib Klasse verwendet. Die Klasse umfasst alle Funktionen der Bibliothek.

Sie beinhaltet die Testskripts testclasses.mq5 und testinterfaces.mq5 sowie ein einfaches Demo-Skript usealglib.mq5. Die gleichnamigen Include-Dateien (testclasses.mqh und testinterfaces.mqh) werden für das Starten von Testfällen verwendet. Speichern Sie diese in \MQL5\Scripts\Alglib\Testcases\.

Aus den zahlreichen Dateien und Funktionen, die die Funktion umfasst, brauchen wir:

Pakete 
Beschreibung 
alglib.mqh
Das Basispaket der Bibliothek beinhaltet benutzerdefinierte Funktionen. Für das Arbeiten mit der Bibliothek müssen diese Funktionen aufgerufen werden.
dataanalysis.mqhKlassen der Datenanalyse:
  1. CMLPBase — mehrschichtiges Perzeptron.
  2. CMLPTrain — Bildung eines mehrschichtigen Perzeptrons.
  3. CMLPE — Sets von neuronalen Netzen.

Beim Herunterladen der Bibliothek werden die Dateien in MQL5\Include\Math\Alglib\ gespeichert. Um die Bibliothek zu verwenden, reicht es den folgenden Befehl in den Programmcode hinzuzufügen:

#include <Math\Alglib\alglib.mqh>

Aus allen Funktionen in diesen zwei Dateien verwenden wir die Funktionen der CAlglib Klasse.

//--- create neural networks
static void    MLPCreate0(const int nin,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreate1(const int nin,int nhid,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreate2(const int nin,const int nhid1,const int nhid2,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreateR0(const int nin,const int nout,double a,const double b,CMultilayerPerceptronShell &network);
static void    MLPCreateR1(const int nin,int nhid,const int nout,const double a,const double b,CMultilayerPerceptronShell &network);
static void    MLPCreateR2(const int nin,const int nhid1,const int nhid2,const int nout,const double a,const double b,CMultilayerPerceptronShell &network);
static void    MLPCreateC0(const int nin,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreateC1(const int nin,int nhid,const int nout,CMultilayerPerceptronShell &network);
static void    MLPCreateC2(const int nin,const int nhid1,const int nhid2,const int nout,CMultilayerPerceptronShell &network)

Die Funktionen "MLPCreate" erstellen ein neuronales Netz mit linearem Output. Die in diesem Artikel angeführten Beispiele veranschaulichen die Erstellung eines Netzes von diesem Typ.

Die Funktionen "MLPCreateR" erstellen ein neuronales Netz mit einem Ausgabewert im Bereich [a, b].

Die Funktionen MLPCreateC erstellen ein neuronales Netz mit einem Output, der nach "Klassen" klassifiziert wird (zum Beispiel, 0 oder 1; -1, 0 oder 1). 

//--- Properties and error of the neural network
static void    MLPProperties(CMultilayerPerceptronShell &network,int &nin,int &nout,int &wcount);
static int     MLPGetLayersCount(CMultilayerPerceptronShell &network);
static int     MLPGetLayerSize(CMultilayerPerceptronShell &network,const int k);
static void    MLPGetInputScaling(CMultilayerPerceptronShell &network,const int i,double &mean,double &sigma);
static void    MLPGetOutputScaling(CMultilayerPerceptronShell &network,const int i,double &mean,double &sigma);
static void    MLPGetNeuronInfo(CMultilayerPerceptronShell &network,const int k,const int i,int &fkind,double &threshold);
static double  MLPGetWeight(CMultilayerPerceptronShell &network,const int k0,const int i0,const int k1,const int i1);
static void    MLPSetNeuronInfo(CMultilayerPerceptronShell &network,const int k,const int i,int fkind,double threshold);
static void    MLPSetWeight(CMultilayerPerceptronShell &network,const int k0,const int i0,const int k1,const int i1,const double w);
static void    MLPActivationFunction(const double net,const int k,double &f,double &df,double &d2f);
static void    MLPProcess(CMultilayerPerceptronShell &network,double &x[],double &y[]);
static double  MLPError(CMultilayerPerceptronShell &network,CMatrixDouble &xy,const int ssize);
static double  MLPRMSError(CMultilayerPerceptronShell &network,CMatrixDouble &xy,const int npoints);



//--- training neural networks
static void    MLPTrainLM(CMultilayerPerceptronShell &network,CMatrixDouble &xy,const int npoints,const double decay,const int restarts,int &info,CMLPReportShell &rep);
static void    MLPTrainLBFGS(CMultilayerPerceptronShell &network,CMatrixDouble &xy,const int npoints,const double decay,const int restarts,const double wstep,int maxits,int &info,CMLPReportShell &rep);

Mithilfe von diesen Funktionen kann man solche neuronalen Netze erstellen und optimieren, die von zwei bis zu vier Schichten beinhalten (Eingabeschicht, Ausgabeschicht, Null-Schicht und eine bis zwei versteckte Schichten). Die Namen der wichtigsten Eingabeparameter:

  • nin: Anzahl der Neuronen in der Eingabeschicht.
  • nout: Ausgabeschicht.
  • nhid1: versteckte Schicht 1.
  • nhid2: versteckte Schicht 2.
  • network: Objekt der CMultilayerPerceptronShell Klasse, das die Definition der Verbindungen und Gewichte zwischen den Neuronen und ihre Aktivierungsfunktionen beinhalten wird.
  • xy: Objekt der CMatrixDouble Klasse, das die Ein- und Ausgabedaten für die Durchführung des Trainigs und der Optimierung des neuronalen Netzes enthalten wird.

Das Training/die Optimierung wird mithilfe des Algorithmus Levenberg-Marquardt (MLPTrainLM()) oder L-BFGS durchgeführt, geregelt durch (MLPTrainLBFGS()). Der letzte Algorithmus wird verwendet, wenn das Netz über 500 Verbindungen/Gewichte umfasst: die Funktion warnt "für Netze mit hunderten Gewichten". Diese Algorithmen sind effizienter, als die sogenannte Backpropagation-Methode, die gewöhnlich in neuronalen Netzen verwendet wird. Die Bibliothek bietet auch andere Optimierungsfunktionen. Der Leser kann diese in Betracht ziehen, wenn er mit den oben genannten Funktionen seine Ziele nicht erreicht hat.

 

3. Implementierung in MQL

Definieren wir die Anzahl der Neuronen in jeder Schicht als externe Eingabeparameter. Des Weiteren definieren wir die Variablen, die die Parameter der Normalisierung beinhalten.

input int nNeuronEntra= 35;      //Anzahl der Neuronen in der Eingabeschicht 
input int nNeuronSal= 1;         //Anzahl der Neuronen in der Eingabeschicht
input int nNeuronCapa1= 45;      //Anzahl der Neuronen in der versteckten Schicht 1 (<1 nicht existiert)
input int nNeuronCapa2= 10;      //Anzahl der Neuronen in der versteckten Schicht 2 (<1 nicht existiert)

input string intervEntrada= "0;1";        //Normalisierung des Inputs: das gewünschte Min. und Max. (leer= wird NICHT normalisiert)
input string intervSalida= "";            //Normalisierung des Outputs: das gewünschte Min. und Max. (leer= wird NICHT normalisiert)

Weitere externe Variablen:

input int velaIniDesc= 15;
input int historialEntrena= 1500;

Diese helfen Ihnen die Balkennummer (velaIniDesc), ab der Unload historischer Daten für das Training des Netzes beginnen soll, sowie die allgemeine Anzahl der Daten des Balkens für den Unload (historialEntrena) anzugeben.

Definieren wir das Netzobjekt und das Objekt des Doppelarrays "arDatosAprende" als offene globale Variablen. 
CMultilayerPerceptronShell *objRed;
CMatrixDouble arDatosAprende(0, 0);

"arDatosAprende" wird die Strings der Ein-/Ausgabedaten für das Training des Netzes umfassen. Das ist eine zweidimensionale dynamische Matrix vom Typ double (wie wir wissen, darf man in mql5 nur eindimensionale dynamische Arrays erstellen. Bei der Erstellung mehrdimensionaler Arrays sind alle Dimensionen außer der ersten anzugeben). 

Die 1.-4. Punkte des Basisalgorithmus werden in der Funktion "gestionRed()" implementiert.

//---------------------------------- ERSTELLT UND OPTIMIERT DAS NEURONALE NETZ --------------------------------------------------
bool gestionRed(CMultilayerPerceptronShell &objRed, string simb, bool normEntrada= true , bool normSalida= true,
                bool imprDatos= true, bool barajar= true)
{
   double tasaAprende= 0.001;             //Koeffizient des Trainings
   int ciclosEntren= 2;                   //Anzahl der Trainingszyklen
   ResetLastError();
   bool creada= creaRedNeuronal(objRed);                                //Erstellung des neuronalen Netzes
  if(creada) 
   {
      preparaDatosEntra(objRed, simb, arDatosAprende);                  //Ein-/Ausgabedaten in arDatosAprende laden
      if(imprDatos) imprimeDatosEntra(simb, arDatosAprende);            //Daten ausgeben, um die Zuverlässigkeit zu überprüfen
      if(normEntrada || normSalida) normalizaDatosRed(objRed, arDatosAprende, normEntrada, normSalida); //optionale Normalisierung der Ein-/Ausgabedaten
      if(barajar) barajaDatosEntra(arDatosAprende, nNeuronEntra+nNeuronSal);    //Strings des Daten-Arrays durchsuchen
      errorMedioEntren= entrenaEvalRed(objRed, arDatosAprende, ciclosEntren, tasaAprende);      //Training/Optimierung durchführen
      salvaRedFich(arObjRed[codS], "copiaSegurRed_"+simb);      //das Netz in einer Datei auf der Festplatte speichern
   }
   else infoError(GetLastError(), __FUNCTION__);
   
   return(_LastError==0);
}


Hier erstellen wir das neuronale Netz in der Funktion (creaRedNeuronal(objRed)); dann laden wir Daten in arDatosAprende mithilfe der preparaDatosEntra() Funktion. Man kann die Daten mithilfe der imprimeDatosEntra() Funktion ausgeben, um die Zuverlässigkeit der Daten abzuschätzen. Wenn die Ein-/Ausgabedaten normalisiert werden müssen, verwenden wir die normalizaDatosRed() Funktion. Wenn wir vor der Optimierung Strings des Daten-Arrays durchsuchen möchten, führen wir barajaDatosEntra() aus. Das Training wird unter Verwendung von entrenaEvalRed() durchgeführt, die Funktion liefert den erhaltenen Optimierungsfehler. Schließlich speichern wir das Netz auf der Platte für eine eventuelle Wiederherstellung, ohne es erneut erstellen und optimieren zu müssen.

Am Anfang der gestionRed() Funktion gibt es zwei Variablen (tasaAprende und ciclosEntrena), die den Koeffizient und die Zyklen des Trainings des neuronalen Netzes definieren. ALGLIB warnt, dass es in der Regel die Werte verwendet werden, die die Funktion wiederspiegelt. Aber bei der Durchführung zahlreicher Tests mit zwei vorgeschlagenen Optimierungsalgorithmen hat sich erwiesen, dass die Änderung der Werte dieser Variablen keinen Einfluss auf die Ergebnisse hat. Zuerst haben wir diese Parameter als Inputparameter eingegeben, aber da sie nicht so wichtig sind, habe ich diese innerhalb der Funktion platziert. Allerdings kann der Leser selbst entscheiden, ob er diese als Eingabeparameter setzt oder nicht.

Die normalizaDatosRed() Funktion wird für die Normalisierung der Trainingsdaten des neuronalen Netzes innerhalb eines bestimmten Bereichs verwendet, vorausgesetzt, dass die echten Daten für den Abruf der Prognose des neuronalen Netzes auch in diesem Bereich liegen. Sonst ist es nicht notwendig, die Daten zu normalisieren. Darüber hinaus werden die echten Daten vor dem Abruf der Prognose nicht normalisiert, wenn die Trainings-Daten bereits normalisiert wurden.

3.1 Erstellung eines neuronalen Netzes 

//--------------------------------- ERSTELLT DAS NEURONALE NETZ --------------------------------------
bool creaRedNeuronal(CMultilayerPerceptronShell &objRed)
{
   bool creada= false;
   int nEntradas= 0, nSalidas= 0, nPesos= 0;
   if(nNeuronCapa1<1 && nNeuronCapa2<1) CAlglib::MLPCreate0(nNeuronEntra, nNeuronSal, objRed);   //LINEARE AUSGABE   
   else if(nNeuronCapa2<1) CAlglib::MLPCreate1(nNeuronEntra, nNeuronCapa1, nNeuronSal, objRed);   //LINEARE AUSGABE
   else CAlglib::MLPCreate2(nNeuronEntra, nNeuronCapa1, nNeuronCapa2, nNeuronSal, objRed);   		//LINEARE AUSGABE                    
   creada= existeRed(objRed);
   if(!creada) Print("Fehler der  Erstellung des neuronalen Netzes ==> ", __FUNCTION__, " ", _LastError);
   else
   {
      CAlglib::MLPProperties(objRed, nEntradas, nSalidas, nPesos);
      Print("Es wurde ein Netz mit N Schichten erstellt", propiedadRed(objRed, N_CAPAS));
      Print("N Neuronen in der Eingabeschicht", nEntradas);
      Print("N Neuronen in der versteckten Schicht 1", nNeuronCapa1);
      Print("N Neuronen in der versteckten Schicht 2 ", nNeuronCapa2);
      Print("N Neuronen in der Ausgabeschicht", nSalidas);
      Print("N Gewichte", nPesos);
   }
   return(creada);
}

Die oben angeführte Funktion erstellt ein neuronales Netz mit der benötigten Anzahl von Schichten und Neuronen (nNeuronEntra, nNeuronCapa1, nNeuronCapa2, nNeuronSal), und überprüft mithilfe der folgenden Funktion, ob das Netz richtig erstellt wurde:

//--------------------------------- BESTEHENDES NETZ  --------------------------------------------
bool existeRed(CMultilayerPerceptronShell &objRed)
{
   bool resp= false;
   int nEntradas= 0, nSalidas= 0, nPesos= 0;
   CAlglib::MLPProperties(objRed, nEntradas, nSalidas, nPesos);
   resp= nEntradas>0 && nSalidas>0;
   return(resp);
}

Wenn das Netz richtig erstellt wurde, informiert die Funktion den Nutzer seine Parameter mithilfe der MLPProperties() Funktion der CAlglib Klasse, die in der ALGLIB Bibliothek enthalten ist.

Wie es bereits im zweiten Abschnitt erwähnt wurde, verfügt ALGLIB über andere Funktionen, mithilfe von welchen man ein neuronales Netz für Klassifikation oder ein Netz für die Lösung von Regressionsproblemen (ausgegeben wird ein konkreter Wert) erstellen kann.

Nach der Erstellung des neuronalen Netzes kann die propiedadRed() Funktion definiert werden, um einige der Parameter in anderen Teilen des Expert Advisors zu erhalten:

enum mis_PROPIEDADES_RED {N_CAPAS, N_NEURONAS, N_ENTRADAS, N_SALIDAS, N_PESOS};

//---------------------------------- NETZEIGENSCHAFTEN -------------------------------------------
int propiedadRed(CMultilayerPerceptronShell &objRed, mis_PROPIEDADES_RED prop= N_CAPAS, int numCapa= 0)
{           //wenn man die Anzahl der Neuronen N_NEURONAS abruft, muss man die Nummer der numCapa Schicht angeben
   int resp= 0, numEntras= 0, numSals= 0, numPesos= 0;
   if(prop>N_NEURONAS) CAlglib::MLPProperties(objRed, numEntras, numSals, numPesos);    
   switch(prop)
   {
      case N_CAPAS:
         resp= CAlglib::MLPGetLayersCount(objRed);
         break;
      case N_NEURONAS:
         resp= CAlglib::MLPGetLayerSize(objRed, numCapa);
         break;
      case N_ENTRADAS:
         resp= numEntras;
         break;
      case N_SALIDAS:
         resp= numSals;
         break;
      case N_PESOS:
         resp= numPesos;
   }
   return(resp);
}  

3.2  Vorbereitung von Ein-/Ausgabedaten 

Die Funktion kann variieren je nach dem wie viel und welche Eingabedaten verwendet werden.

//---------------------------------- BEREITET EINGABE-/AUSGABEDATEN VOR --------------------------------------------------
void preparaDatosEntra(CMultilayerPerceptronShell &objRed, string simb, CMatrixDouble &arDatos, bool normEntrada= true , bool normSalida= true)
{
   int fin= 0, fila= 0, colum= 0,
       nEntras= propiedadRed(objRed, N_ENTRADAS),
       nSals= propiedadRed(objRed, N_SALIDAS);
   double valor= 0, arResp[];   
   arDatos.Resize(historialEntrena, nEntras+nSals);
   fin= velaIniDesc+historialEntrena;
   for(fila= velaIniDesc; fila<fin; fila++)
   {                   
      for(colum= 0; colum<NUM_INDIC;  colum++)
      {
         valor= valorIndic(codS, fila, colum);
         arDatos[fila-1].Set(colum, valor);
      }
      calcEstrat(fila-nVelasPredic, arResp);
      for(colum= 0; colum<nSals; colum++) arDatos[fila-1].Set(colum+nEntras, arResp[colum]);
   }
   return;
}

Während des Prozesses durchlaufen wir die ganze Historie von velaIniDesc bis velaIniDesc+historialEntrena und erhalten den Wert des (NUM_INDIC) Indikators auf jedem Balken, danach laden wir ihn in die entsprechende Spalte der binären Matrix CMatrixDouble. Geben wir das Ergebnis der Strategie ("calcEstrat()") für jeden Balken ein. Das Ergebnis entspricht den eingegebenen Werten der Indikatoren. Die nVelasPredic Variable erlaubt es, diese Indikatorwerte um n-Kerzen nach vorne zu extrapolieren. Gewöhnlich wird nVelasPredic als externer Parameter definiert.

D.h. jede Zeile des "arDatos" Arrays der CMatrixDouble Klasse wird so viele Spalten enthalten, wie viele Eingabedaten oder Indikatorwerte in der Strategie verwendet werden und wie viele Ausgabedaten definiert sind. Das arDatos Array wird so viele Zeilen beinhalten, wie viele durch den Wert in historialEntrena definiert wurden.

3.3 Ausgabe des Arrays der Ein-/Ausgabedaten

Wenn der Inhalt der zweidimensionalen Matrix ausgegeben werden muss, um die Genauigkeit von Ein-/Ausgabedaten zu überprüfen, kann die imprimeDatosEntra() Funktion verwendet werden.

//----------------------------------  GIBT INPUT-/OUTPUTDATEN AUS --------------------------------------------------
void imprimeDatosEntra(string simb, CMatrixDouble &arDatos)
{
   string encabeza= "indic1;indic2;indic3...;resultEstrat",     //Namen der Indikatoren, getrennt durch ";" 
          fichImprime= "dataEntrenaRed_"+simb+".csv";
   bool entrar= false, copiado= false;
   int fila= 0, colum= 0, resultEstrat= -1, nBuff= 0,
       nFilas= arDatos.Size(),
       nColum= nNeuronEntra+nNeuronSal,
       puntFich= FileOpen(fichImprime, FILE_WRITE|FILE_CSV|FILE_COMMON);
   FileWrite(puntFich, encabeza);
   for(fila= 0; fila<nFilas; fila++)
   {
      linea= IntegerToString(fila)+";"+TimeToString(iTime(simb, PERIOD_CURRENT, velaIniDesc+fila), TIME_MINUTES)+";";                
      for(colum= 0; colum<nColum;  colum++) 
         linea= linea+DoubleToString(arDatos[fila][colum], 8)+(colum<(nColum-1)? ";": "");
      FileWrite(puntFich, linea);
   }
   FileFlush(puntFich);
   FileClose(puntFich);
   Alert("Download file= ", fichImprime);
   Alert("Path= ", TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\Files");
   return;
}

Die Funktion durchsucht die Matrix zeilenweise und erstellt bei jedem Schritt die Zeile "línea" mit allen Spaltenwerten, voneinander durch ";" getrennt. Dann werden diese Daten in einer .csv-Datei gespeichert, welche mithilfe der FileOpen() Funktion erstellt wird. Für das Thema dieses Artikels spielt diese Funktion eine sekundäre Rolle, deswegen werden wir diese nicht kommentieren. Für die Überprüfung von Dateien vom Typ .csv kann man Excel verwenden.

3.4  Normalisierung der Daten innerhalb eines bestimmten Intervalls

Es gilt als sinnvoll, die Inputdaten vor der Optimierung des Netzes in einem bestimmten Bereich zu platzieren, d.h. die Daten müssen normalisiert werden. Dafür bietet sich die folgende Funktion, die Ein-/Ausgabedaten (wahlweise) aus dem Array arDatos der Klasse CMatrixDouble normalisiert.

//------------------------------------ NORMALISIERT EIN-/AUSGABEDATEN -------------------------------------
void normalizaDatosRed(CMultilayerPerceptronShell &objRed, CMatrixDouble &arDatos, bool normEntrada= true, bool normSalida= true)
{
   int fila= 0, colum= 0, maxFila= arDatos.Size(),
       nEntradas= propiedadRed(objRed, N_ENTRADAS),
       nSalidas= propiedadRed(objRed, N_SALIDAS);
   double maxAbs= 0, minAbs= 0, maxRel= 0, minRel= 0, arMaxMinRelEntra[], arMaxMinRelSals[];
   ushort valCaract= StringGetCharacter(";", 0);
   if(normEntrada) StringSplit(intervEntrada, valCaract, arMaxMinRelEntra);
   if(normSalida) StringSplit(intervSalida, valCaract, arMaxMinRelSals);
   for(colum= 0; normEntrada && colum<nEntradas; colum++)
   {
      maxAbs= arDatos[0][colum];
      minAbs= arDatos[0][colum];
      minRel= StringToDouble(arMaxMinRelEntra[0]);
      maxRel= StringToDouble(arMaxMinRelEntra[1]); 
      for(fila= 0; fila<maxFila; fila++)                // maxAbs und minAbs für jede Spalte definieren
      {
         if(maxAbs<arDatos[fila][colum]) maxAbs= arDatos[fila][colum];
         if(minAbs>arDatos[fila][colum]) minAbs= arDatos[fila][colum];            
      }
      for(fila= 0; fila<maxFila; fila++)                //den neuen normalisierten Wert setzen
         arDatos[fila].Set(colum, normValor(arDatos[fila][colum], maxAbs, minAbs, maxRel, minRel));
   }
   for(colum= nEntradas; normSalida && colum<(nEntradas+nSalidas); colum++)
   {
      maxAbs= arDatos[0][colum];
      minAbs= arDatos[0][colum];
      minRel= StringToDouble(arMaxMinRelSals[0]);
      maxRel= StringToDouble(arMaxMinRelSals[1]);
      for(fila= 0; fila<maxFila; fila++)
      {
         if(maxAbs<arDatos[fila][colum]) maxAbs= arDatos[fila][colum];
         if(minAbs>arDatos[fila][colum]) minAbs= arDatos[fila][colum];            
      }
      minAbsSalida= minAbs;
      maxAbsSalida= maxAbs;
      for(fila= 0; fila<maxFila; fila++)
         arDatos[fila].Set(colum, normValor(arDatos[fila][colum], maxAbs, minAbs, maxRel, minRel));
   }
   return;
}

Wenn man sich dafür entschieden hat, die Trainings-Inputdaten innerhalb eines bestimmten Bereichs zu normalisieren, wird die Iteration über die Werte erneut durchlaufen. Es ist wichtig sicherzustellen, dass die echten Daten, die für den Abruf der Prognose des neuronalen Netzes weiter verwendet werden, auch innerhalb dieses Bereiches liegen. Sonst ist es nicht notwendig, die Normalisierung durchzuführen.

Man darf nicht vergessen, dass intervEntrada und intervSalida String-Variablen sind, die als Eingabeparameter definiert wurden (s. Anfang des Abschnittes "Implementierung in MQL5"). Sie können, z.B., so aussehen: "0;1" oder "-1;1". D.h. sie beinhalten relative Maxima und Minima. Die Funktion "StringSplit()" übergibt den String an das Array, das diese relativen Extrema beinhalten wird. Für jede Spalte muss man:

  1. Das absolute Minimum und Maximum festlegen (die Variablen maxAbs und minAbs).
  2. Die ganze Spalte durchsuchen und die Werte zwischen "maxRel" und "minRel" normalisieren: s. unten die Funktion normValor().
  3. Den neuen normalisierten Wert mithilfe der .set Methode der CMatrixDouble Klasse in arDatos setzen.
//------------------------------------ NORMALISIERUNGSFUNKTION ---------------------------------
double normValor(double valor, double maxAbs, double minAbs, double maxRel= 1, double minRel= -1)
{
   double valorNorm= 0;
   if(maxAbs>minAbs) valorNorm= (valor-minAbs)*(maxRel-minRel))/(maxAbs-minAbs) + minRel;
   return(valorNorm);
}
3.5 Iteration über Ein-/Ausgabedaten

Um eine eventuelle Vererbung von Daten innerhalb des Arrays zu vermeiden, können wir die Reihenfolge der Strings innerhalb des Arrays wahlweise ändern (iterieren). Verwenden wir dafür die Funktion barajaDatosEntra, die über die Strings des CMatrixDouble Arrays iteriert und für jeden String einen neuen Target-String definiert. Dabei wird die Position der Daten in jeder Spalte eingehalten, und die Daten werden mithilfe der Blasenmethode (filaTmp Variable) übertragen. 

//------------------------------------ ITERIERT ÜBER EIN-/AUSGABEDATEN  ZEILENWEISE -----------------------------------
void barajaDatosEntra(CMatrixDouble &arDatos, int nColum)
{
   int fila= 0, colum= 0, filaDestino= 0, nFilas= arDatos.Size();
   double filaTmp[];
   ArrayResize(filaTmp, nColum);
   MathSrand(GetTickCount());          //setzt eine beliebige untergeordnete Serie zurück
   while(fila<nFilas)
   {
      filaDestino= randomEntero(0, nFilas-1);   //erhält zufälligerweise einen neuen Target-String 
      if(filaDestino!=fila)
      {
         for(colum= 0; colum<nColum; colum++) filaTmp[colum]= arDatos[filaDestino][colum];
         for(colum= 0; colum<nColum; colum++) arDatos[filaDestino].Set(colum, arDatos[fila][colum]);
         for(colum= 0; colum<nColum; colum++) arDatos[fila].Set(colum, filaTmp[colum]);
         fila++;
      }
   }
   return;
}

Nach der Zurücksetzung der beliebigen untergeordneter Serie "MathSrand(GetTcikCount())" wird die Funktion "randomEntero()" regeln, wohin genau die Strings zufälligerweise verschoben werden.

//---------------------------------- ZUFÄLLIGE VERSCHIEBUNG -----------------------------------------------
int randomEntero(int minRel= 0, int maxRel= 1000)
{
   int num= (int)MathRound(randomDouble((double)minRel, (double)maxRel));
   return(num);
}

3.6  Training/Optimierung des neuronalen Netzes
Die ALGLIB Bibliothek erlaubt es, Algorithmen der Netzkonfiguration zu verwenden, welche die Trainings- und Optimierungszeit im Vergleich zu Backpropagation, dem traditionellen System, das in mehrschichtigem Perzeptron eingesetzt wird, reduzieren. Wie bereits erwähnt wurde, verwenden wir:

  • den Algorithmus Levenberg-Marquardt mit Regularisierung und einer genauen Berechnung der Hesse-Matrix (MLPTrainLM()), oder
  • den Algorithmus L-BFGS mit der Regularisierung (MLPTrainLBFGS()).

Der zweite Algorithmus wird für die Optimierung eines Netzes mit über 500 Gewichten verwendet.

//---------------------------------- TRAINING DES NETZES -------------------------------------------
double entrenaEvalRed(CMultilayerPerceptronShell &objRed, CMatrixDouble &arDatosEntrena, int ciclosEntrena= 2, double tasaAprende= 0.001)
{
   bool salir= false;
   double errorMedio= 0; string mens= "Entrenamiento Red";
   int k= 0, i= 0, codResp= 0,
       historialEntrena= arDatosEntrena.Size();
   CMLPReportShell infoEntren;
   ResetLastError();
   datetime tmpIni= TimeLocal();
   Alert("Optimierung des neuronales Netzes...");
   Alert("Bitte warten Sie einige Minuten je nach dem Umfang der angewandten Historie.");
   Alert("...///...");
   if(propiedadRed(objRed, N_PESOS)<500)
      CAlglib::MLPTrainLM(objRed, arDatosEntrena, historialEntrena, tasaAprende, ciclosEntrena, codResp, infoEntren);
   else
      CAlglib::MLPTrainLBFGS(objRed, arDatosEntrena, historialEntrena, tasaAprende, ciclosEntrena, 0.01, 0, codResp, infoEntren);
   if(codResp==2 || codResp==6) errorMedio= CAlglib::MLPRMSError(objRed, arDatosEntrena, historialEntrena);
   else Print("Cod entrena Resp: ", codResp);
   datetime tmpFin= TimeLocal();
   Alert("NGrad ", infoEntren.GetNGrad(), " NHess ", infoEntren.GetNHess(), " NCholesky ", infoEntren.GetNCholesky());
   Alert("codResp ", codResp," Mittlerer Fehler des Trainings"+DoubleToString(errorMedio, 8), " ciclosEntrena ", ciclosEntrena);
   Alert("tmpEntren ", DoubleToString(((double)(tmpFin-tmpIni))/60.0, 2), " min", "---> tmpIni ", TimeToString(tmpIni, _SEG), " tmpFin ", TimeToString(tmpFin, _SEG));
   infoError(GetLastError(), __FUNCTION__);
   return(errorMedio);
}

Wie wir sehen, bekommt die Funktion das "Objekt des Netzes" und die bereits normalisierte Matrix der Ein-/Ausgabedaten als Eingabeparameter. Wir definieren auch die Zyklen oder "Epochen" des Trainings ("ciclosEntrena"; oder wie viele Male der Algorithmus eine Anpassung in der Suche nach dem Trainingsfehler durchführt, der am wenigsten wahrscheinlich ist); die Dokumentation empfiehlt 2. Die von mir durchgeführten Tests haben bei der Erhöhung der Epochenzahl keine besseren Ergebnisse gezeigt. Wir haben auch den Parameter "Trainingskoeffizient" ("tasaAprende") erwähnt.

Definieren wir das Objekt "infoEntren" (der CMLPReportShell Klasse)  am Anfang der Funktion. Das Objekt wird Informationen über das Trainingsergebnis sammeln. Danach erhalten wir es mithilfe der Methoden GetNGrad() und GetNCholesky(). Der mittlere Trainingsfehler (der mittlere quadratische Fehler aller Output-Daten hinsichtlich der Input-Daten, die nach der Verarbeitung durch den Algorithmus erhalten wurden) wird mithilfe der Funktion "MLPRMSError()" erhalten. Darüber hinaus informieren wir den Nutzer über die für die Optimierung verbrauchte Zeit. Dafür nehmen wir den Anfangs- und Endzeitpunkt in den Variablen tmpIni und tmpFin.

Diese Optimierungsfunktionen liefern den Code des Ausführungsfehlers "codResp", der die folgenden Werte annehmen kann:

  • -2, wenn das Trainingsmodell mehr Ausgabedaten als die Anzahl der Neuronen in der Ausgabeschicht hat.
  • -1, wenn irgendein Inputparameter der Funktion ungültig ist.
  • 2, richtige Ausführung, der Fehler ist kleiner, als das Stopp-Kriterium ("MLPTrainLM()").
  • 6, das gleiche gilt für die Funktion "MLPTrainLBFGS()".

Auf diese Weise liefert die richtige Ausführung 2 oder 6 entsprechend den Gewichten des optimierten neuronalen Netzes. 

Diese Algorithmen konfigurieren das Netz so, dass die Reiteration der Trainigszyklen (Variable "ciclosEntrena") keinen Einfluss auf den Fehler hat, im Gegensatz zum Algorthmus "Back Propagation", wo die Reiteration die Genauigkeit wesentlich beeinflussen kann. Ein Netz aus 4 Schichten mit 35, 45, 10 und 2 Neuronen und einer Eingabematrix aus 2000 Zeilen kann mithilfe der oben beschriebenen Funktion innerhalb von 4-6 Minuten (I5, core 4, RAM 8 gb) mit einem Fehler von 2-4 Hunderttausendstel (4x10^-5) optimiert werden.

3.7 Speicherung des Netzes in einer Textdatei oder Wiederherstellung

Wir haben eine neuronales Netz erstellt, Ein-/Ausgabedaten vorbereitet und das Training durchgeführt. Als Vorsichtsmaßnahme sollte man das Netz auf der Platte speichern, für den Fall wenn beim Laufen des Expert Advisors unerwartete Fehler auftreten. Dafür müssen wir Funktionen von ALGLIB verwenden, um Charakteristika sowie interne Werte des Netzes (Anzahl von Schichten und Neuronen in jeder Schicht, Gewichte usw.) zu erhalten und diese Daten in einer Textdatei auf der Platte zu speichern.

//-------------------------------- NETZ AUF DER PLATTE SPEICHERN -------------------------------------------------
bool salvaRedFich(CMultilayerPerceptronShell &objRed, string nombArch= "")
{
   bool redSalvada= false;
   int k= 0, i= 0, j= 0, numCapas= 0, arNeurCapa[], neurCapa1= 1, funcTipo= 0, puntFichRed= 9999;
   double umbral= 0, peso= 0, media= 0, sigma= 0;
   if(nombArch=="") nombArch= "copiaSegurRed";
   nombArch= nombArch+".red";
   FileDelete(nombArch, FILE_COMMON);
   ResetLastError();
   puntFichRed= FileOpen(nombArch, FILE_WRITE|FILE_BIN|FILE_COMMON);
   redSalvada= puntFichRed!=INVALID_HANDLE;
   if(redSalvada)
   {
      numCapas= CAlglib::MLPGetLayersCount(objRed);   
      redSalvada= redSalvada && FileWriteDouble(puntFichRed, numCapas)>0;
      ArrayResize(arNeurCapa, numCapas);
      for(k= 0; redSalvada && k<numCapas; k++)
      {
         arNeurCapa[k]= CAlglib::MLPGetLayerSize(objRed, k);
         redSalvada= redSalvada && FileWriteDouble(puntFichRed, arNeurCapa[k])>0;
      }
      for(k= 0; redSalvada && k<numCapas; k++)
      {
         for(i= 0; redSalvada && i<arNeurCapa[k]; i++)
         {
            if(k==0)
            {
               CAlglib::MLPGetInputScaling(objRed, i, media, sigma);
               FileWriteDouble(puntFichRed, media);
               FileWriteDouble(puntFichRed, sigma);
            }
            Selse if(k==numCapas-1)
            {
               CAlglib::MLPGetOutputScaling(objRed, i, media, sigma);
               FileWriteDouble(puntFichRed, media);
               FileWriteDouble(puntFichRed, sigma);
            }
            CAlglib::MLPGetNeuronInfo(objRed, k, i, funcTipo, umbral);
            FileWriteDouble(puntFichRed, funcTipo);
            FileWriteDouble(puntFichRed, umbral);
            for(j= 0; redSalvada && k<(numCapas-1) && j<arNeurCapa[k+1]; j++)
            {
               peso= CAlglib::MLPGetWeight(objRed, k, i, k+1, j);
               redSalvada= redSalvada && FileWriteDouble(puntFichRed, peso)>0;
            }
         }      
      }
      FileClose(puntFichRed);
   }
   if(!redSalvada) infoError(_LastError, __FUNCTION__);
   return(redSalvada);
} 

Wie man im sechsten String des Codes sieht, wird die Datei mit der Dateiendung ".red" versehen, was später die Suche und Überprüfungen erleichtern wird. Wir haben mehrere Stunden für Debugging gebraucht, aber es funktioniert!

Wenn die Arbeit nach einem Event, das den Experten gestoppt hat, fortgesetzt werden muss, stellen wir das Netz aus der Datei mithilfe einer zu der oben beschriebenen gegensätzlichen Funktion wiederher. Diese Funktion erstellt das Objekt des Netzes und füllt es mit Daten, indem sie die Daten aus der Textdatei liest, in welcher wir das neuronale Netz gespeichert haben.

//-------------------------------- STELLT DAS NETZ VON DER PLATTE WIEDERHER  -------------------------------------------------
bool recuperaRedFich(CMultilayerPerceptronShell &objRed, string nombArch= "")
{
   bool exito= false;
   int k= 0, i= 0, j= 0, nEntradas= 0, nSalidas= 0, nPesos= 0,
       numCapas= 0, arNeurCapa[], funcTipo= 0, puntFichRed= 9999;
   double umbral= 0, peso= 0, media= 0, sigma= 0;
   if(nombArch=="") nombArch= "copiaSegurRed";
   nombArch= nombArch+".red";
   puntFichRed= FileOpen(nombArch, FILE_READ|FILE_BIN|FILE_COMMON);
   exito= puntFichRed!=INVALID_HANDLE;
   if(exito)
   {
      numCapas= (int)FileReadDouble(puntFichRed);
      ArrayResize(arNeurCapa, numCapas);
      for(k= 0; k<numCapas; k++) arNeurCapa[k]= (int)FileReadDouble(puntFichRed); 
      if(numCapas==2) CAlglib::MLPCreate0(nNeuronEntra, nNeuronSal, objRed);
      else if(numCapas==3) CAlglib::MLPCreate1(nNeuronEntra, nNeuronCapa1, nNeuronSal, objRed);
      else if(numCapas==4) CAlglib::MLPCreate2(nNeuronEntra, nNeuronCapa1, nNeuronCapa2, nNeuronSal, objRed);
      exito= existeRed(arObjRed[0]);
      if(!exito) Print("Fehler bei der Erstellung des neuronalen Netzes ==> ", __FUNCTION__, " ", _LastError);
      else
      {
         CAlglib::MLPProperties(objRed, nEntradas, nSalidas, nPesos);
         Print("Es wurde ein Netz mit N Schichten wiederhergestellt", propiedadRed(objRed, N_CAPAS));
         Print("N Neuronen in der Eingabeschicht", nEntradas);
         Print("N Neuronen in der versteckten Schicht 1", nNeuronCapa1);
         Print("N Neuronen in der versteckten Schicht 2 ", nNeuronCapa2);
         Print("N Neuronen in der Ausgabeschicht", nSalidas);
         Print("N Gewichte", nPesos);
         for(k= 0; k<numCapas; k++)
         {
            for(i= 0; i<arNeurCapa[k]; i++)
            {
               if(k==0)
               {
                  media= FileReadDouble(puntFichRed);
                  sigma= FileReadDouble(puntFichRed);
                  CAlglib::MLPSetInputScaling(objRed, i, media, sigma);
               }
               Selse if(k==numCapas-1)
               {
                  media= FileReadDouble(puntFichRed);
                  sigma= FileReadDouble(puntFichRed);
                  CAlglib::MLPSetOutputScaling(objRed, i, media, sigma);
               }
               funcTipo= (int)FileReadDouble(puntFichRed);
               umbral= FileReadDouble(puntFichRed);
               CAlglib::MLPSetNeuronInfo(objRed, k, i, funcTipo, umbral);
               for(j= 0; k<(numCapas-1) && j<arNeurCapa[k+1]; j++)
               {
                  peso= FileReadDouble(puntFichRed);
                  CAlglib::MLPSetWeight(objRed, k, i, k+1, j, peso);
               }
            }      
         }
      }
   }
   FileClose(puntFichRed);
   return(exito);
} 

Um die Prognose des Netzes beim Laden der Werte zu erhalten, wird die Funktion "respuestaRed()" aufgerufen:

//--------------------------------------- FORDERT DIE ANTWORT DES NETZES AN  ---------------------------------
double respuestaRed(CMultilayerPerceptronShell &ObjRed, double &arEntradas[], double &arSalidas[], bool desnorm= false)
{
   double resp= 0, nNeuron= 0;
   CAlglib::MLPProcess(ObjRed, arEntradas, arSalidas);   
   if(desnorm)             //Wenn die Normalisierung von Ausgabedaten geändert werden muss
   {
      nNeuron= ArraySize(arSalidas);
      for(int k= 0; k<nNeuron; k++)
         arSalidas[k]= desNormValor(arSalidas[k], maxAbsSalida, minAbsSalida, arMaxMinRelSals[1], arMaxMinRelSals[0]);
   }
   resp= arSalidas[0];
   return(resp);
}

Diese Funktion verfügt über die Möglichkeit, Normalisierung von Ausgabedaten in der Trainingsmatrix zu ändern.

 

4.  Selbstoptimierung

Nachdem der Expert Advisor das neuronale Netz (und Eingabewerte für den Expert Advisor) während seiner Ausführung und nicht im Rahmen der Optimierung im Strategientester optimiert hat, muss der im ersten Abschnitt beschriebene Basisalgorithmus wiederholt werden.

Außerdem werden wir mit einer wichtigen Aufgabe konfrontiert: der Expert Advisor muss ununterbrochen den Markt kontrollieren und darf diese Kontrolle während der Optimierung des neuronalen Netzes, welche die Verwendung großer Rechnerkapazitäten voraussetzt, nicht verlieren.

Setzen wir den Aufzählungstyp "mis_PLAZO_OPTIM". Dieser Aufzählunfstyp beschreibt Intervalle, welche der Nutzer für die Wiederholung des Basisalgorithmus (täglich, wahlweise oder am Wochenende) verwenden kann. Setzen wir auch eine andere Aufzählung, damit der Nutzer selbst entscheiden kann, ob der Expert Advisor als "Optimierer" das Netz optimiert oder als "Vollzieher" die Strategie umsetzt.

enum mis_PLAZO_OPTIM {_DIARIO, _DIA_ALTERNO, _FIN_SEMANA};
enum mis_TIPO_EAred {_OPTIMIZA, _EJECUTA};

Wie Sie sich vielleicht erinnern, ermöglicht МetaTrader 5 eine gleichzeitige Ausführung eines Expert Advisors auf jedem offenen Chart. Starten wir den Expert Advisor im Umsetzungsmodus auf dem ersten Chart und im Optimierungsmodus - auf dem zweiten. Auf dem ersten Chart wird der EA die Strategie kontrollieren, und auf dem zweiten - das neuronale Netz optimieren. Auf diese Weise wird das zweite oben beschriebene Problem gelöst. Auf dem ersten Chart "verwendet" der Expert Advisor das neuronale Netz, indem er es aus der Textdatei liest, welche er jedes mal bei der Optimierung des neuronalen Netzes im Optimierungsmodus generiert.

Früher haben wir gesagt, dass Optimierungstests von 4 bis 6 Minuten der Berechnungszeit in Anspruch nehmen. Die Anwendung dieser Methode dauert etwas länger: von 8 bis 15 Minuten je nach der asiatischen oder europäischen Handelszeit, die Strategie wird aber weiter kontrolliert.

Definieren wir die Eingabeparameter, um das umzusetzen.

input mis_TIPO_EAred tipoEAred            = _OPTIMIZA;        //Typ der auszuführenden Aufgabe
input mis_PLAZO_OPTIM plazoOptim          = _DIARIO;          //Zeitintervall für die Optimierung des Netzes
input int horaOptim                       = 3;                //Ortszeit für die Optimierung des Netzes

Der Parameter "horaOptim" speichert die Ortszeit, nach welcher die Optimierung durchgeführt werden muss. Diese Zeit muss mit einer geringen oder keiner Marktaktivität zusammenfallen: in Europa. z.B. am frühen Morgen (03:00 Uhr als Standardwert) oder an einem Wochenende. Wenn man Optimierung immer beim Starten des Expert Advisors durchführen will, ohne auf vorgegebene Zeit und Tag zu warten, muss man das Folgende setzen:

input bool optimInicio                    = true;         //Das neuronale Netz beim Starten des Expert Advisors optimieren

Um zu kontrollieren, ob das Netz als optimiert gilt (Optimierungsmodus), und die Zeit des letzten Lesens der Netz-Datei (Umsetzungsmodus) zu bestimmen, definieren wir die folgenden Variablen:

double fechaUltLectura;
bool reOptimizada= false;

Für die Lösung des ersten Problems wird der Verarbeitungsblock der angegebenen Methode in der OnTimer() Funktion gesetzt, welche entsprechend der tmp Periode mindestens jede Stunde ausgeführt wird. Die tmp Periode wurde mithilfe von EventSetTimer(tmp) in OnInit() gesetzt. Auf diese Weise wird der "Optimierer" jede tmp Sekunden überprüfen, ob das Netz neu optimiert werden muss, und der Experte im Umsetzungsmodus überprüft, ob die Netz-Datei wieder gelesen werden muss, falls diese durch den "Optimierer" aktualisiert wurde.

/---------------------------------- ON TIMER --------------------------------------
void OnTimer()
{
   bool existe= false;
   string fichRed= "";
   if(tipoEAred==_OPTIMIZA)            //der EA arbeitet im Optimierungsmodus
   {
      bool optimizar= false;
      int codS= 0,
          hora= infoFechaHora(TimeLocal(), _HORA);    //die ganze aktuelle Zeit erhalten
      if(!redOptimizada) optimizar= horaOptim==hora && permReoptimDia();
      fichRed= "copiaSegurRed_"+Symbol()+".red";      //definiert den Dateinamen des neuronalen Netzes
      existe= buscaFich(fichRed, "*.red");            //sucht nach der Datei, wo das neuronale Netz gespeichert wurde
      if(!existe || optimizar)
         redOptimizada= gestionRed(objRed, simb, intervEntrada!="", intervSalida!="", imprDatosEntrena, barajaDatos);
      if(hora>(horaOptim+6)) redOptimizada= false;    //nach sechs Stunden von der vorgesehenen Zeit gilt das optimierte Netz als veraltet
      guardaVarGlobal(redOptimizada);                 //speichert den Wert "reoptimizada" (neu optimiert) auf der Platte
   }
   else if(tipoEAred==_EJECUTA)        //der Expert Advisor arbeitet im Umsetzungsmodus
   {
      datetime fechaUltOpt= 0;
      fichRed= "copiaSegurRed_"+Symbol()+".red";      //definiert den Dateinamen des neuronalen Netzes
      existe= buscaFich(fichRed, "*.red");            //sucht nach der Datei, wo das neuronale Netz gespeichert wurde
      if(existe)
      {
         fechaUltOpt= fechaModifFich(0, fichRed);     //bestimmt das Datum der letzten Optimierung (Änderung der Netz-Datei)
         if(fechaUltOpt>fechaUltLectura)              //wenn das Optimierungsdatum älter als das Datum des letzten Lesens ist
         {
            recuperaRedFich(objRed, fichRed);         //liest und generiert ein neues neuronales Netz
            fechaUltLectura= (double)TimeCurrent();
            guardaVarGlobal(fechaUltLectura);         //speichert das neue Lesedatum auf der Platte
            Print("Das Netz wurde nach der Optimierung wiederhergestellt... "+simb);      //gibt die Meldung auf den Bildschirm aus
         }
      }
      else Alert("tipoEAred==_EJECUTA --> Datei des neuronalen Netzes nicht gefunden: "+fichRed+".red");
   }
   return;
}

Weitere zusätzliche Funktionen, die hier nicht kommentiert werden:

//--------------------------------- ERLAUBT EINE ERNEUTE OPTIMIERUNG ---------------------------------
bool permReoptimDia()
{
   int diaSemana= infoFechaHora(TimeLocal(), _DSEM);
   bool permiso= (plazoOptim==_DIARIO && diaSemana!=6 && diaSemana!=0) ||     //optimiert [jeden Tag von Dienstag bis Samstag]
                 (plazoOptim==_DIA_ALTERNO && diaSemana%2==1) ||              //optimiert [Dienstag, Donnerstag und Samstag]
                 (plazoOptim==_FIN_SEMANA && diaSemana==5);                   //optimiert [Samstag]
   return(permiso);
}

//-------------------------------------- SUCHT NACH DER DATEI --------------------------------------------
bool buscaFich(string fichBusca, string filtro= "*.*", int carpeta= FILE_COMMON)
{
   bool existe= false;
   string fichActual= "";
   long puntBusca= FileFindFirst(filtro, fichActual, carpeta);
   if(puntBusca!=INVALID_HANDLE)
   {
      ResetLastError();
      while(!existe)
      {
         FileFindNext(puntBusca, fichActual);
         existe= fichActual==fichBusca;
      }
      FileFindClose(puntBusca);
   }
   else Print("Datei nicht gefunden!");
   infoError(_LastError, __FUNCTION__);
   return(existe);

Der beschriebene Algorithmus wird momentan in dem EA verwendet, den wir testen. So können wir die Strategie völlig kontrollieren, während das Netz jede Nacht ab 3:00 Uhr Ortszeit mit den Н1-Daten für die letzten drei Monate neu optimiert wird: 35 Neuronen in der Eingabeschicht, 45 Neuronen in der versteckten Schicht 1, 8 in der versteckten Schicht 2 und zwei in der Ausgabeschicht; die Optimierung dauert ca. 35-45 Minuten.

5. Aufgabe 1: binär-dezimaler Umrechner

Um dieses System zu überprüfen, lösen wir eine Aufgabe, deren genaue Lösung uns im Voraus bekannt ist (es gibt einen entsprechenden Algorithmus), und vergleichen wir diesen Wert mit dem Wert, den das neuronale Netz liefert. Erstellen wir einen binär-dezimalen Umrechner. Für den Test wird das folgende Skript verwendet:

#property script_show_confirm
#property script_show_inputs

#define FUNC_CAPA_OCULTA   1  
#define FUNC_SALIDA        -5
            //1= Hyperbaltangens; 2= e^(-x^2); 3= x>=0 raizC(1+x^2) x<0 e^x; 4= Sigmoidfunktion;
            //5= binomische x>0.5? 1: 0; -5= lineare Funktion
#include <Math\Alglib\alglib.mqh>

enum mis_PROPIEDADES_RED {N_CAPAS, N_NEURONAS, N_ENTRADAS, N_SALIDAS, N_PESOS};
//---------------------------------  Inputparameter  ---------------------
sinput int nNeuronEntra= 10;                 //Anzahl der Neuronen in der Eingabeschicht 
                                             //2^8= 256 2^9= 512; 2^10= 1024; 2^12= 4096; 2^14= 16384 
sinput int nNeuronCapa1= 0;                  //Anzahl der Neuronen in der versteckten Schicht 1 (<1 nicht existiert)
sinput int nNeuronCapa2= 0;                  //Anzahl der Neuronen in der versteckten Schicht 2 (<1 nicht existiert)                                             //2^8= 256 2^9= 512; 2^10= 1024; 2^12= 4096; 2^14= 16384 
sinput int nNeuronSal= 1;                    //Anzahl  der Neuronen in der Eingabeschicht

sinput int    historialEntrena= 800;         //Historie des Trainings
sinput int    historialEvalua= 200;          //Historie der Bewertung
sinput int    ciclosEntrena= 2;              //Trainigszyklen
sinput double tasaAprende= 0.001;            //Trainingsniveau 
sinput string intervEntrada= "";             //Normalisierung des Inputs: das gewünschte Min. und Max. (leer= wird NICHT normalisiert)
sinput string intervSalida= "";              //Normalisierung des Outputs: das gewünschte Min. und Max. (leer= wird NICHT normalisiert)
sinput bool   imprEntrena= true;             //Daten des Trainings/der Auswertung ausgeben
      
// ------------------------------ GLOBALE VARIABLEN -----------------------------     
int puntFichTexto= 0;
ulong contFlush= 0; 
CMultilayerPerceptronShell redNeuronal;
CMatrixDouble arDatosAprende(0, 0);
CMatrixDouble arDatosEval(0, 0);
double minAbsSalida= 0, maxAbsSalida= 0;
string nombreEA= "ScriptBinDec";

//+------------------------------------------------------------------+
void OnStart()              //Binär-dezimaler Umrechner
{
   string mensIni= "Script conversor BINARIO-DECIMAL",
          mens= "", cadNumBin= "", cadNumRed= "";
   int contAciertos= 0, arNumBin[],
       inicio= historialEntrena+1,
       fin= historialEntrena+historialEvalua;
   double arSalRed[], arNumEntra[], salida= 0, umbral= 0, peso= 0;
   double errorMedioEntren= 0;
   bool normEntrada= intervEntrada!="", normSalida= intervSalida!="", correcto= false,
        creada= creaRedNeuronal(redNeuronal);        
   if(creada) 
   {
      iniFichImprime(puntFichTexto, nombreEA+"-infRN", ".csv",mensIni);
      preparaDatosEntra(redNeuronal, arDatosAprende, intervEntrada!="", intervSalida!="");
      normalizaDatosRed(redNeuronal, arDatosAprende, normEntrada, normSalida);
      errorMedioEntren= entrenaEvalRed(redNeuronal, arDatosAprende);
      escrTexto("-------------------------", puntFichTexto);
      escrTexto("RESPUESTA RED------------", puntFichTexto);
      escrTexto("-------------------------", puntFichTexto);
      escrTexto("numBinEntra;numDecSalidaRed;correcto", puntFichTexto);
      for(int k= inicio; k<=fin; k++)
      {
         cadNumBin= dec_A_baseNumerica(k, arNumBin, 2, nNeuronEntra);
         ArrayCopy(arNumEntra, arNumBin);
         salida= respuestaRed(redNeuronal, arNumEntra, arSalRed);
         salida= MathRound(salida);
         correcto= k==(int)salida;
         escrTexto(cadNumBin+";"+IntegerToString((int)salida)+";"+correcto, puntFichTexto);
         cadNumRed= "";
      }
   }      
   deIniFichImprime(puntFichTexto);
   return;
}

Nachdem wir das neuronale Netz erstellt haben, führen wir ein Training mit den ersten 800 natürlichen Zahlen in Binärform (10 Zeichen, 10 Input-Neuronen und 1 Output-Neuron) durch. Dann wandeln wir die nächsten 200 natürlichen Zahlen in Binärzahlen um (von 801 bis 1000 in Binärform) und vergleichen das Ergebnis mit dem, welches das neuronale Netz vorhergesagt hat. Zum Beispiel, wenn wir 1100110100 setzen (820 binär; 10 Zeichen, 10 Input-Neuronen), muss das Netz 820 oder eine ähnliche Zahl als Ergebnis liefern. Die oben beschriebene For Methode ist für das Erhalten der Netzprognose für diese 200 Zahlen und für den Vergleich des erwarteten Ergebnisses mit dem vorhergesagten zuständig.

Nach der Ausführung des Skripts mit den gesetzten Parametern (neuronales Netz ohne versteckte Schichten, 10 Input-Neuronen und 1 Output-Neuron) bekommen wir ein hervorragendes Ergebnis. Die im Ordner Terminal\Common\Files generierte Datei ScriptBinDec-infRN.csv gibt uns die folgenden Informationen:


 

Auf dem Bild sehen wir, dass das Skript die Trainingsmatrix bis 800 binär (Input) und dezimal (Output) ausgegeben hat. Das neuronale Netz wurde trainiert, und es hat das Ergebnis ab 801 ausgegeben. In der dritten Spalte haben wir true bekommen. So haben wir das erwartete und erhaltene Ergebnis miteinander. Wie bereits gesagt wurde, ist das ein gutes Ergebnis.

Nichtsdestotrotz wenn wir die Struktur des neuronalen Netzes als "10 Input-Neuronen, 20 Neuronen in der versteckten Schicht 1, 1 Output-Neuron" definieren, bekommen wir das folgende:


 

Das Ergebnis ist inakzeptabel! Wir werden mit einem großen Problem bei der Verarbeitung des neuronalen Netzes konfrontiert: welche interne Konfiguration (Anzahl der Schichten, der Neuronen und Aktivierungsfunktionen) passt am besten? Nur Erfahrung, tausende Tests und solche Artikel wie Bewertung und Auswahl von Variablen für Modelle für maschinelles Lernen helfen, dieses Problem zu lösen. Darüber hinaus haben wir die Daten der Trainingsmatrix im Programm der statistischen Analyse Rapid Miner verwendet, um zu versuchen, die effektivste Struktur zu finden, bevor diese in mql5 umzusetzen.

 

6. Aufgabe 2: Detektor der Primzahlen

Betrachten wir eine ähnliche Aufgabe. Dieses Mal muss das neuronale Netz feststellen, ob eine Zahl zu Primzahlen gehört oder nicht. Die Trainings-Matrix enthält 10 Spalten mit 10 Zeichen jeder Primzahl in Binärform bis 800 und eine Spalte, in welcher es angegeben wird, ob die Zahl zu Primzahlen gehört ("1") oder nicht ("0"). Mit anderen Worten: wir haben 800 Zeilen und 11 Spalten. Lassen wir das neuronale Netz weitere 200 natürliche Zahlen in Binärform (von 801 bis 1000) analysieren und feststellen, welche Zahl eine Primzahl ist und welche nicht. Da diese Aufgabe schwieriger ist, geben wir die Statistik der Übereinstimmungen aus.

#include <Math\Alglib\alglib.mqh>

enum mis_PROPIEDADES_RED {N_CAPAS, N_NEURONAS, N_ENTRADAS, N_SALIDAS, N_PESOS};
//---------------------------------  Inputparameter  ---------------------
sinput int nNeuronEntra= 10;                 //Anzahl der Neuronen in der Eingabeschicht
                                             //2^8= 256 2^9= 512; 2^10= 1024; 2^12= 4096; 2^14= 16384 
sinput int nNeuronCapa1= 20;                 //Anzahl der Neuronen in der versteckten Schicht 1 (<1 nicht existiert)
sinput int nNeuronCapa2= 0;                  //Anzahl der Neuronen in der versteckten Schicht 2 (<1 nicht existiert)                                             //2^8= 256 2^9= 512; 2^10= 1024; 2^12= 4096; 2^14= 16384 
sinput int nNeuronSal= 1;                    //Anzahl  der Neuronen in der Eingabeschicht

sinput int    historialEntrena= 800;         //Historie des Trainings
sinput int    historialEvalua= 200;          //Historie der Prognose
sinput int    ciclosEntrena= 2;              //Trainigszyklen
sinput double tasaAprende= 0.001;            //Koeffizient des Trainings
sinput string intervEntrada= "";             //Normalisierung des Inputs: das gewünschte Min. und Max. (leer= wird NICHT normalisiert)
sinput string intervSalida= "";              //Normalisierung des Outputs: das gewünschte Min. und Max. (leer= wird NICHT normalisiert)
sinput bool   imprEntrena= true;             //Daten des Trainings/der Auswertung ausgeben
      
// ------------------------------ GLOBALE VARIABLEN -----------------------------     
int puntFichTexto= 0;
ulong contFlush= 0; 
CMultilayerPerceptronShell redNeuronal;
CMatrixDouble arDatosAprende(0, 0);
double minAbsSalida= 0, maxAbsSalida= 0;
string nombreEA= "ScriptNumPrimo";

//+----------------------- Detektor der Primzahlen -------------------------------------------+
void OnStart()
{
   string mensIni= "Script comprobación NÚMEROS PRIMOS", cadNumBin= "", linea= "";
   int contAciertos= 0, totalPrimos= 0, aciertoPrimo= 0, arNumBin[],
       inicio= historialEntrena+1,
       fin= historialEntrena+historialEvalua;
   double arSalRed[], arNumEntra[], numPrimoRed= 0;
   double errorMedioEntren= 0;
   bool correcto= false,
        esNumPrimo= false, 
        creada= creaRedNeuronal(redNeuronal);        
   if(creada) 
   {
      iniFichImprime(puntFichTexto, nombreEA+"-infRN", ".csv",mensIni);
      preparaDatosEntra(redNeuronal, arDatosAprende, intervEntrada!="", intervSalida!="");
      normalizaDatosRed(redNeuronal, arDatosAprende, normEntrada, normSalida);
      errorMedioEntren= entrenaEvalRed(redNeuronal, arDatosAprende);
      escrTexto("-------------------------", puntFichTexto);
      escrTexto("RESPUESTA RED------------", puntFichTexto);
      escrTexto("-------------------------", puntFichTexto);
      escrTexto("numDec;numBin;numPrimo;numPrimoRed;correcto", puntFichTexto);
      for(int k= inicio; k<=fin; k++)
      {
         cadNumBin= dec_A_baseNumerica(k, arNumBin, 2, nNeuronEntra);
         esNumPrimo= esPrimo(k);
         ArrayCopy(arNumEntra, arNumBin);
         numPrimoRed= respuestaRed(redNeuronal, arNumEntra, arSalRed);
         numPrimoRed= MathRound(numPrimoRed);
         correcto= esNumPrimo==(int)numPrimoRed;
         if(esNumPrimo)
         {
            totalPrimos++;
            if(correcto) aciertoPrimo++;  
         }
         if(correcto) contAciertos++;
         linea= IntegerToString(k)+";"+cadNumBin+";"+esNumPrimo+";"+(numPrimoRed==0? "false": "true")+";"+correcto;
         escrTexto(linea, puntFichTexto);
      }
   }     
   escrTexto("porc Aciertos / total;"+DoubleToString((double)contAciertos/(double)historialEvalua*100, 2)+" %", puntFichTexto); 
   escrTexto("Aciertos primos;"+IntegerToString(aciertoPrimo)+";"+"total primos;"+IntegerToString(totalPrimos), puntFichTexto); 
   escrTexto("porc Aciertos / total primos;"+DoubleToString((double)aciertoPrimo/(double)totalPrimos*100, 2)+" %", puntFichTexto); 
   deIniFichImprime(puntFichTexto);
   return;
}

Nach der Ausführung des Skripts mit den angegebenen Parametern (Netz ohne versteckte Schichten, 10 Input-Neuronen, 20 Neuronen in der versteckten Schicht 1 und 1 Output-Neuron) bekommen wir ein schlechteres Ergebnis als vorher. Die im Ordner Terminal\Common\Files generierte Datei "ScriptNumPrimo-infRN.csv" gibt uns die folgenden Informationen:


 

Wie wir sehen können, hat das Netz die erste Primzahl nach 800 (809) nicht gefunden (richtig = falsch). Statistischer Überblick:


 

Hier sehen wir, dass das neuronale Netz 78% aus 200 Zahlen innerhalb des Bewertungsintervalls (801 bis 200) richtig geraten hat. Aber aus 29 Primarzahlen in diesem Bereich hat es nur 13 (44,83 %) gefunden.

Wenn wir einen Test mit der folgenden Netzstruktur durchführen: 10 Neuronen in der Eingabeschicht, 35 in der versteckten Schicht 1, 10 in der versteckten Schicht 2 und 1 in der Ausgabeschicht, gibt das Skript während der Ausführung die Daten aus:


 

Auf dem Bild unten sieht man, mit 0,53 Minuten und einem mittleren Trainingsfehler von 0,04208383, haben sich die Ergebnisse verschlechtert.


 

Auf diese Weise stellt sich wieder die Frage: wie definiert man die interne Struktur eines Netzes am besten?

 

Fazit

In der Suche nach einem selbstoptimierenden Expert Advisor haben wir den Optimierungscode eines neuronalen Netzes aus der ALGLIB Bibliothek in MQL5 implementiert. Wir haben eine Lösung des Problems vorgeschlagen, das den EA stört, seine Handelsstrategie während einer Netzkonfiguration zu kontrollieren, die viele Ressourcen braucht.

Dann haben wir einen Teil des vorgeschlagenen Codes für die Lösung von zwei Aufgaben von einem MQL5-Programm verwendet: binär-dezimale Umwandlung, Erkennung von Primzahlen und Betrachtung der Ergebnisse entsprechend der internen Struktur des neuronalen Netzes.

Ob dieses Material zur Implementierung einer profitablen Handelsstrategie beiträgt? Das wissen wir noch nicht, aber wir arbeiten daran. Fürs Erste beschränken wir uns auf diesem Beitrag.