English Русский 中文 Español 日本語 Português
Verwenden von OpenCL, um Kerzenmuster zu testen

Verwenden von OpenCL, um Kerzenmuster zu testen

MetaTrader 5Beispiele | 21 Januar 2019, 11:12
873 0
Serhii Shevchuk
Serhii Shevchuk

Einführung

Wenn Händler anfangen, OpenCL zu beherrschen, stehen sie vor der Frage, wie sie es einsetzen sollen. Solche anschaulichen Beispiele wie die Multiplikation von Matrizen oder das Sortieren großer Datenmengen werden bei der Entwicklung von Indikatoren oder automatisierten Handelssystemen nicht häufig verwendet. Eine weitere gängige Anwendung — das Arbeiten mit neuronalen Netzen — erfordert bestimmte Kenntnisse in diesem Bereich. Das Studium neuronaler Netze kann für einen gewöhnlichen Programmierer viel Zeit kosten, ohne Ergebnisse beim Handel zu garantieren. Dieser Umstand kann diejenigen enttäuschen, die die volle Kraft von OpenCL bei der Lösung von elementaren Aufgaben spüren möchten.

In diesem Artikel werden wir den Einsatz von OpenCL zur Lösung der einfachsten Aufgabe des algorithmischen Handels betrachten — das Auffinden von Kerzenmustern und das Testen in der Vergangenheit. Wir werden den Algorithmus zum Testen eines einzelnen Durchlaufs und zur Optimierung zweier Parameter im Handelsmodus "1 Minute OHLC" entwickeln. Danach werden wir die Leistung des integrierten Strategietesters mit der von OpenCL vergleichen und herausfinden, welcher von ihnen (und in welchem Maße) schneller ist.

Es wird davon ausgegangen, dass der Leser bereits mit den Grundlagen von OpenCL vertraut ist. Ansonsten empfehle ich die Artikel "OpenCL: Die Brücke zu parallelen Welten" und "OpenCL: Vom naiven zum aufschlussreicheren Programmieren". Es wäre auch gut, die OpenCL Specification Version 1.2 zur Hand zu haben. Der Artikel konzentriert sich auf den Algorithmus zum Aufbau eines Testers, ohne sich mit den Grundlagen der OpenCL-Programmierung zu befassen.



1. Implementation in MQL5

Wir müssen uns auf etwas verlassen können, um sicherzustellen, dass die Implementierung des Testers auf OpenCL korrekt funktioniert. Zuerst werden wir einen MQL5-EA entwickeln. Dann werden wir die Ergebnisse der Prüfung und Optimierung mit einem normalen Tester mit denen des OpenCL-Testers vergleichen.

Das Testobjekt ist ein einfacher EA, der mit den folgenden Kerzenmustern handelt.
  • Bearish (abwärts) Pin-Bar
  • Bullish (aufwärts) Pin-Bar
  • Bearish Engulfing (umhüllend)
  • Bullish Engulfing (umhüllend)

Die Strategie ist einfach:

  • Bearish Pin-Bar oder Bearish Engulfing  — verkaufen
  • Bullish Pin-Bar oder Bullish Engulfing — kaufen
  • Die Anzahl gleichzeitig offenere Positionen — unbegrenzt
  • Maximale Haltezeit der Positionen — begrenzt, nutzerdefiniert
  • Take-Profit und Stop-Loss — unveränderlich, nutzerdefiniert

Das Vorhandensein eines Musters ist mit komplett geschlossenen Bars zu überprüfen. Mit anderen Worten wir suchen die Muster mittels drei vorherigen Bars sobald eine neue Bar erscheint.

Die Bedingung der Mustererkennung ist wie folgt:

Pin-Bar

Abb. 1. Die Muster von "Bearish Pin-Bar" (a) und "Bullish Pin-Bar" (b)

Für die Bearish Pin-Bar (Fig. 1, a):

  • Der obere Schatten ("tail") der ersten Bar ist größer als der Referenzwert: tail>=Reference
  • Die Bar 0 ist bullish: Close[0]>Open[0]
  • Die zweite Bar ist bearish: Open[2]>Close[2]
  • Das Hoch der ersten Bar ist das lokale Maximum: High[1]>MathMax(High[0],High[2])
  • Der Körper der ersten Bar ist kleiner als der obere Schatten: MathAbs(Open[1]-Close[1])<tail
  • tail = High[1]-max(Open[1],Close[1])

Für die Bullish Pin-Bar (Fig. 1, b):

  • Der untere Schatten ("tail") der ersten Bar ist größer als der Referenzwert: tail>=Reference
  • Die Bar 0 ist bearish: Open[0]>Close[0]
  • Die zweite Bar ist bullish: Close[2]>Open[2]
  • Das Tief der ersten Bar ist das lokale Minimum: Low[1]<MathMin(Low[0],Low[2])
  • Der Körper der ersten Bar ist kleiner als der untere Schatten: MathAbs(Open[1]-Close[1])<tail
  • tail = min(Open[1],Close[1])-Low[1]


Engulfing

Abb. 2. "Bearish Engulfing" (a) und "Bullish Engulfing" (b)

Für Bearish Engulfing (Fig. 2, a):

  • Die erste Bar ist bullish, ihr Körper ist größer als der Referenzwert: (Close[1]-Open[1])>=Reference
  • Das Hoch der Bar Null ist kleiner als der Schlusskurs der ersten Bar: High[0]<Close[1]
  • Der Eröffnungspreis der zweiten Bar überschreitet den Schlusskurs der ersten Bar: Open[2]>CLose[1]
  • Der Schlusskurs der zweiten Bar ist kleiner als der Eröffnungspreis der ersten Bar: Close[2]<Open[1]

Für Bullish Engulfing (Fig. 2, b):

  • Die erste Bar ist bearish, ihr Körper ist größer als der Referenzwert: (Close[1]-Open[1])>=Reference
  • Das Tief der Bar Null ist größer als der Schlusskurs der ersten Bar: Low[0]>Close[1]
  • Der Eröffnungspreis der zweiten Bar unterschreitet den Schlusskurs der ersten Bar: Open[2]<Close[1]
  • Der Schlusskurs der zweiten Bar ist größer als der Eröffnungspreis der ersten Bar: Close[2]>Open[1]


1.1 Die Suche nach den Mustern

Der Code für die Definition der Muster ist unten dargestellt.
ENUM_PATTERN Check(MqlRates &r[],uint flags,double ref)
  {
//--- Bearish Pin-Bar  
   if((flags&PAT_PINBAR_BEARISH)!=0)
     {//
      double tail=H(1)-MathMax(O(1),C(1));
      if(tail>=ref && C(0)>O(0) && O(2)>C(2) && H(1)>MathMax(H(0),H(2)) && MathAbs(O(1)-C(1))<tail)
         return PAT_PINBAR_BEARISH;
     }
//--- Bullish Pin-Bar
   if((flags&PAT_PINBAR_BULLISH)!=0)
     {//
      double tail=MathMin(O(1),C(1))-L(1);
      if(tail>=ref && O(0)>C(0) && C(2)>O(2) && L(1)<MathMin(L(0),L(2)) && MathAbs(O(1)-C(1))<tail)
         return PAT_PINBAR_BULLISH;
     }
//--- Bearish Engulfing
   if((flags&PAT_ENGULFING_BEARISH)!=0)
     {//
      if((C(1)-O(1))>=ref && H(0)<C(1) && O(2)>C(1) && C(2)<O(1))
         return PAT_ENGULFING_BEARISH;
     }
//--- Bullish Engulfing
   if((flags&PAT_ENGULFING_BULLISH)!=0)
     {//
      if((O(1)-C(1))>=ref && L(0)>C(1) && O(2)<C(1) && C(2)>O(1))
         return PAT_ENGULFING_BULLISH;
     }
//--- nichts gefunden   
   return PAT_NONE;
  }

Hier sollten wir die Enumeration ENUM_PATTERN anschauen. Die Werte sind wie Flags, die kombiniert und als Argument mittels bitwise OR übergeben werden können:

enum ENUM_PATTERN
  {
   PAT_NONE=0,
   PAT_PINBAR_BEARISH = (1<<0),
   PAT_PINBAR_BULLISH = (1<<1),
   PAT_ENGULFING_BEARISH = (1<<2),
   PAT_ENGULFING_BULLISH = (1<<3)
  };

Wir haben auch die Möglichkeit zu Makros für einen kompakten Code:

#define O(i) (r[i].open)
#define H(i) (r[i].high)
#define L(i) (r[i].low)
#define C(i) (r[i].close)

Die Funktion Check() wird von der Funktion IsPattern() aufgerufen, um das Vorhandensein eines angegebenen Musters beim Erscheinen einer neuen Bar zu überprüfen:

ENUM_PATTERN IsPattern(uint flags,uint ref)
  {
   MqlRates r[];
   if(CopyRates(_Symbol,_Period,1,PBARS,r)<PBARS)
      return 0;
   ArraySetAsSeries(r,false);
   return Check(r,flags,double(ref)*_Point);
  }


1.2 Zusammenbau des EAs

Als erstes müssen die Eingabeparameter definiert werden. Wir aben einen Referenzwert in den Bedingungen für die Muster. Es ist dies die kleinste Länge des "tail" einer Pin-Bar oder die sich kreuzenden Körper bei Engulfing. Wir geben das in Points an:

input int      inp_ref=50;

Außerdem haben wir eine Reihe von Mustern, mit denen wir arbeiten. Aus Gründen der Übersichtlichkeit werden wir das Register der Flags in den Eingängen nicht verwenden. Stattdessen werden wir es in vier Parameter vom Typ bool unterteilen:

input bool     inp_bullish_pin_bar = true;
input bool     inp_bearish_pin_bar = true;
input bool     inp_bullish_engulfing = true;
input bool     inp_bearish_engulfing = true;

Und kombinieren sie in eine unsignierten Variablen in der Initialisierungsfunktion:

   p_flags = 0;
   if(inp_bullish_pin_bar==true)
      p_flags|=PAT_PINBAR_BULLISH;
   if(inp_bearish_pin_bar==true)
      p_flags|=PAT_PINBAR_BEARISH;
   if(inp_bullish_engulfing==true)
      p_flags|=PAT_ENGULFING_BULLISH;
   if(inp_bearish_engulfing==true)
      p_flags|=PAT_ENGULFING_BEARISH;

Als nächstes bestimmen wir die maximal erlaubte Haltezeit der Positionen in Stunden, Take-Profit und Stop-Loss und auch das Volumen.

input int      inp_timeout=5;
input bool     inp_bullish_pin_bar = true;
input bool     inp_bearish_pin_bar = true;
input bool     inp_bullish_engulfing = true;
input bool     inp_bearish_engulfing = true;
input double   inp_lot_size=1;
Für den Handel verwenden wir die Klasse CTrade aus der Standardbibliothek. Um die Testergeschwindigkeit zu definieren, verwenden wir die Klasse CDuration, die es ermöglicht, Zeitintervalle zwischen Kontrollpunkten der Programmausführung in Mikrosekunden zu messen und in einer komfortablen Form anzuzeigen. In diesem Fall messen wir die Zeit zwischen den Funktionen OnInit() und OnDeinit(). Der vollständige Klassencode ist in der angehängten Datei Duration.mqh enthalten.
CDuration time;

int OnInit()
  {
   time.Start();
   // ... 
   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason)
  {
   time.Stop();
   Print("Test lasted "+time.ToStr());
  }

Der EA ist exterm einfach und besteht aus dem Folgenden.

Die Hauptaufgabe der Funktion OnTick() ist die Bearbeitung offener Positionen. Sie schließt eine Position, wenn deren Haltezeit den in den Eingängen angegebenen Wert überschreitet. Anschließend wird das Öffnen eines neuen Balkens überprüft. Wenn die Prüfung bestanden ist, wird das Vorhandensein des Musters mit der Funktion IsPattern() überprüft. Wird ein Muster gefunden, wird eine Kauf- oder Verkaufsposition gemäß der Strategie eröffnet. Der vollständige Funktionscode von OnTick() ist unten aufgeführt:

void OnTick()
  {
//--- bearbeiten der offenen Positionen
   int total= PositionsTotal();
   for(int i=0;i<total;i++)
     {
      PositionSelect(_Symbol);
      datetime t0=datetime(PositionGetInteger(POSITION_TIME));
      if(TimeCurrent()>=(t0+(inp_timeout*3600)))
        {
         trade.PositionClose(PositionGetInteger(POSITION_TICKET));
        }
      else
         break;
     }
   if(IsNewBar()==false)
      return;
//--- prüfen auf vorhandene Muster
   ENUM_PATTERN pat=IsPattern(p_flags,inp_ref);
   if(pat==PAT_NONE)
      return;
//--- Positionseröffnung
   double ask=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   double bid=SymbolInfoDouble(_Symbol,SYMBOL_BID);
   if((pat&(PAT_ENGULFING_BULLISH|PAT_PINBAR_BULLISH))!=0)//Kauf     
      trade.Buy(inp_lot_size,_Symbol,ask,NormalizeDouble(ask-inp_sl*_Point,_Digits),NormalizeDouble(ask+inp_tp*_Point,_Digits),DoubleToString(ask,_Digits));
   else//Verkauf
      trade.Sell(inp_lot_size,_Symbol,bid,NormalizeDouble(bid+inp_sl*_Point,_Digits),NormalizeDouble(bid-inp_tp*_Point,_Digits),DoubleToString(bid,_Digits));
  }


1.3 Tests

Zuerst einmal, starten wir die Optimierung, um die besten Eingabewerte für den EA herauszufinden, um profitabel zu handeln oder zumindest die Positionen zu eröffnen. Wir werden zwei Parameter optimieren — einen Referenzwert für Muster und den Stop-Loss in Points. Setzen wir Take-Profit Level auf 50 Points und wählen alle Muster zum Testen aus.

Die Optimierung soll mit EURUSD M5 durchgeführt werden. Zeitintervall: 01.01.2018 — 01.10.2018. Schnelle Optimierung (genetischer Algorithmus), Handelsmodus: "1 Minute OHLC".

Die Werte der optimierten Parameter werden in einem weiten Bereich mit einer großen Anzahl von Abstufungen ausgewählt:


Abb. 3. Optimierungsparameter


Nach der Optimierung werden die Werte nach dem Gewinn sortiert:


Abb. 4. Optimierungsergebnisse


Wie man sehen kann, erzielten wir das beste Ergebnis mit einen Gewinn von 1000,50 bei einem Referenzwert von 60 Points und einem Stop-Loss von 350 Points. Starten wir den Test mit diesen drei Parameterwerten und achten auf die Ausführungszeit.



Abb. 5. Der Testzeitraum eines einzelnen Durchlaufn im integrierten Tester


Wir merken uns diese Werte und fahren fort mit den Tests derselben Strategie ohne den normalen Tester zu verwenden. Entwickeln wir unseren eigenen Tester unter der Verwendung von OpenCL.


2. Implementation in OpenCL

Um mit OpenCL zu arbeiten, werden wir die Klasse COpenCL aus der Standardbibliothek mit kleinen Änderungen verwenden. Ziel der Verbesserungen ist es, so viele Informationen wie möglich über auftretende Fehler zu erhalten. Dabei sollten wir den Code jedoch nicht durch Bedingungen und die Ausgabe von Daten an die Konsole überladen. Dazu erstellen wir die Klasse COpenCLx. Der vollständige Code befindet sich in der unten angehängten Datei OpenCLx.mqh:

class COpenCLx : public COpenCL
  {
private:
   COpenCL          *ocl;
public:
                     COpenCLx();
                    ~COpenCLx();
   STR_ERROR         m_last_error;  // letzter Fehler
   COCLStat          m_stat;        // OpenCL Statistik                     
   //--- Arbeit mit Puffern
   bool              BufferCreate(const ENUM_BUFFERS buffer_index,const uint size_in_bytes,const uint flags,const string function,const int line);
   template<typename T>
   bool              BufferFromArray(const ENUM_BUFFERS buffer_index,T &data[],const uint data_array_offset,const uint data_array_count,const uint flags,const string function,const int line);
   template<typename T>
   bool              BufferRead(const ENUM_BUFFERS buffer_index,T &data[],const uint cl_buffer_offset,const uint data_array_offset,const uint data_array_count,const string function,const int line);
   template<typename T>
   bool              BufferWrite(const ENUM_BUFFERS buffer_index,T &data[],const uint cl_buffer_offset,const uint data_array_offset,const uint data_array_count,const string function,const int line);
   //--- Bestimmen der Argumente
   template<typename T>
   bool              SetArgument(const ENUM_KERNELS kernel_index,const int arg_index,T value,const string function,const int line);
   bool              SetArgumentBuffer(const ENUM_KERNELS kernel_index,const int arg_index,const ENUM_BUFFERS buffer_index,const string function,const int line);
   //--- arbeit mit dem Kernel
   bool              KernelCreate(const ENUM_KERNELS kernel_index,const string kernel_name,const string function,const int line);
   bool              Execute(const ENUM_KERNELS kernel_index,const int work_dim,const uint &work_offset[],const uint &work_size[],const string function,const int line);
   //---
   bool              Init(ENUM_INIT_MODE mode);
   void              Deinit(void);
  };

Wie wir sehen können, enthält die Klasse einen Zeiger auf das Objekt COpenCL sowie mehrere Methoden, die als Wrapper für die gleichnamigen Klassenmethoden COpenCL dienen. Jede dieser Methoden hat den Namen der Funktion und die Zeichenkette, aus der sie unter den Argumenten aufgerufen wurde. Darüber hinaus werden anstelle von Kernel-Indizes und -Puffern Enumeratoren verwendet. Dies geschieht, um die Anwendung von EnumToString() in der Fehlermeldung zu ermöglichen, was viel informativer ist als nur ein Index.

Lassen Sie uns eine dieser Methoden näher betrachten.

bool COpenCLx::KernelCreate(const ENUM_KERNELS kernel_index,const string kernel_name,const string function,const int line)
  {
   if(ocl==NULL)
     {
      SET_UERRx(UERR_NO_OCL,"OpenCL object does not exist",function,line);
      return false;
     }
//--- Starten der Ausführung des Kernels
   ::ResetLastError();
   if(!ocl.KernelCreate(kernel_index,kernel_name))
     {
      string comment="Failed to create kernel "+EnumToString(kernel_index)+", name \""+kernel_name+"\"";
      SET_ERRx(comment,function,line);
      if(!m_last_error.code)
         SET_UERRx(UERR_KERNEL_CREATE,comment,function,line);
      return(false);
     }
//---
   return true;
  }

Es gibt hier zwei Prüfungen: auf das Vorhandensein des Objekts der Klasse COpenCL und des Erfolgs der Kernelerstellungsmethode. Anstatt jedoch mit der Funktion Print() einen Text anzuzeigen, werden die Meldungen zusammen mit dem Fehlercode, dem Funktionsnamen und der Aufrufzeichenfolge an Makros übergeben. Diese Makros speichern Fehlerinformationen in der Klasse m_last_error Klassenfehler. Deren Struktur ist unten dargestellt:

struct STR_ERROR
  {
   int               code;       // Code 
   string            comment;    // Kommentar 
   string            function;   // Funktion des aufgetretenen Fehlers
   int               line;       // Zeile des aufgetretenen Fehlers
  };

Insgesamt gibt es vier Makros. Betrachten wir sie einzeln.

Das Makro SET_ERR schreibt den letzten Ausführungsfehler, Funktion und Zeile, wo es aufgerufen wurde, werden mit dem übergebenen Parameter ausgewiesen:

#define SET_ERR(c) do {m_last_error.function = __FUNCTION__; \
      m_last_error.line =__LINE__; \
      m_last_error.code=::GetLastError(); m_last_error.comment=c;} while(0)

Das Makro SET_ERRx ist ähnlich wie SET_ERR:

#define SET_ERRx(c,f,l) do {m_last_error.function = f; m_last_error.line = l; \
      m_last_error.code=::GetLastError(); m_last_error.comment=c;} while(0)

Es unterscheidet sich dadurch, dass Funktion und Zeile als Parameter übergeben werden. Warum das? Angenommen, es ist ein Fehler in der Methode KernelCreate() aufgetreten. Wenn wir das Makro SET_ERR verwenden, können wir den KernelCreate() Methodennamen sehen, aber es ist viel nützlicher zu wissen, wo die Methode aufgerufen wurde. Um dies zu erreichen, übergeben wir die Funktion und Zeile beim Aufruf der Methode als Argumente dem Makro.

Das Makro SET_UERR kommt als nächstes. Es ist für das Schreiben von nutzerdefinierten Fehlern gedacht:

#define SET_UERR(err,c) do {m_last_error.function = __FUNCTION__; \
      m_last_error.line =__LINE__; \
      m_last_error.code=ERR_USER_ERROR_FIRST+err; m_last_error.comment=c;} while(0)

In dem Makro wird die Fehlernummer als Argument übergeben, statt die Funktion GetLastError() zu verwenden. In gewisser Hinsicht ist es ähnlich dem Makro SET_ERR.

Das Makro SET_UERRx soll die nutzerdefinierten Fehler ausweisen, es werden Funktionsname und Zeile des Aufrufs übergeben:

#define SET_UERRx(err,c,f,l) do {m_last_error.function = f; m_last_error.line = l; \
      m_last_error.code=ERR_USER_ERROR_FIRST+err; m_last_error.comment=c;} while(0)

So haben wir im Fehlerfall alle notwendigen Informationen. Im Gegensatz zu den Fehlern, die von der Klasse COpenCL an die Konsole gesendet werden, ist dies eine Spezifikation des Zielkerns und von wo aus die Methode ihrer Erstellung aufgerufen wurde. Vergleichen wir einfach die Ausgabe der Klasse COpenCL (obere Zeichenkette) und die erweiterte Ausgabe der Klasse COpenCLx (zwei untere Zeilen):


Fehler bei der Kernelerstellung

Abb. 6. Fehler bei der Kernelerstellung

Betrachten wir ein anderes Beispiel einer Wrappermethode: die Methode zum Erstellen des Puffers:

bool COpenCLx::BufferCreate(const ENUM_BUFFERS buffer_index,const uint size_in_bytes,const uint flags,const string function,const int line)
  {
   if(ocl==NULL)
     {
      SET_UERRx(UERR_NO_OCL,"OpenCL object does not exist",function,line);
      return false;
     }
//--- abrufen und prüfen des freien Speichers
   if((m_stat.gpu_mem_usage+=size_in_bytes)==false)
     {
      CMemsize cmem=m_stat.gpu_mem_usage.Comp(size_in_bytes);
      SET_UERRx(UERR_NO_ENOUGH_MEM,"No free GPU memory. Insufficient "+cmem.ToStr(),function,line);
      return false;
     }
//--- Puffer erstellen
   ::ResetLastError();
   if(ocl.BufferCreate(buffer_index,size_in_bytes,flags)==false)
     {
      string comment="Failed to create buffer "+EnumToString(buffer_index);
      SET_ERRx(comment,function,line);
      if(!m_last_error.code)
         SET_UERRx(UERR_BUFFER_CREATE,comment,function,line);
      return(false);
     }
//---
   return(true);
  }

Neben der Überprüfung des Vorhandenseins des Objekts und des Operationsergebnisses der Klasse COpenCL enthält sie auch die Funktion zur Abrechnung und Überprüfung des freien Speichers. Da wir es mit relativ großen Mengen an Speicher (Hunderte von Megabyte) zu tun haben, müssen wir den Prozess des Verbrauchs kontrollieren. Diese Aufgabe ist СMemsize zugeordnet. Der vollständige Code ist in der Datei Memsize.mqh enthalten.

Es gibt jedoch einen Nachteil. Trotz des komfortablen Debuggens wird der Code umständlich. Der Code für die Puffererstellung sieht beispielsweise wie folgt aus:

if(BufferCreate(buf_ORDER_M1,len*sizeof(int),CL_MEM_READ_WRITE,__FUNCTION__,__LINE__)==false)
   return false;

Es gibt zu viele unnötige Informationen, die die Konzentration auf den Algorithmus schwierig machen. Hier kommen uns die Makros wieder einmal zur Hilfe. Jede der Wrapper-Methoden wird durch ein Makro dupliziert, wodurch der Aufruf kompakter wird. Für die Methode BufferCreate() ist es das Makro _BufferCreate:

#define _BufferCreate(buffer_index,size_in_bytes,flags) \
      if(BufferCreate(buffer_index,size_in_bytes,flags,__FUNCTION__,__LINE__)==false) return false

Dank des Makros wird der Aufruf der Methode zum Erstellen der Puffer einfacher:

_BufferCreate(buf_ORDER_M1,len*sizeof(int),CL_MEM_READ_WRITE);

Das Erstellen des Kernels sieht wie folgt aus:

_KernelCreate(k_FIND_PATTERNS,"find_patterns");

Die meisten dieser Makros enden mit return false, während _KernelCreate mit break endet. Dies sollte bei der Entwicklung des Codes berücksichtigt werden. Alle Makros sind in der Datei OCLDefines.mqh definiert.

Die Klasse beinhaltet auch die Initialisierung- und Deinitialisierungsmethoden. Neben dem Erstellen des Klassenobjekts COpenCL überprüft die erste die Unterstützung von 'double', erstellt Kernel und fragt den verfügbaren Speicher ab:

bool COpenCLx::Init(ENUM_INIT_MODE mode)
  {
   if(ocl) Deinit();
//--- erstellen des Objekts der Klasse COpenCL
   ocl=new COpenCL;
   while(!IsStopped())
     {
      //--- initialisieren von OpenCL
      ::ResetLastError();
      if(!ocl.Initialize(cl_tester,true))
        {
         SET_ERR("OpenCL initialization error");
         break;
        }
      //--- prüfen, ob die Arbeit mit 'double' unterstützt wird
      if(!ocl.SupportDouble())
        {
         SET_UERR(UERR_DOUBLE_NOT_SUPP,"Working with double (cl_khr_fp64) is not supported by the device");
         break;
        }
      //---bestimmen der Anzahl der Kernels
      if(!ocl.SetKernelsCount(OCL_KERNELS_COUNT))
         break;
      //--- erstellen der Kernel         
      if(mode==i_MODE_TESTER)
        {
         _KernelCreate(k_FIND_PATTERNS,"find_patterns");
         _KernelCreate(k_ARRAY_FILL,"array_fill");
         _KernelCreate(k_ORDER_TO_M1,"order_to_M1");
         _KernelCreate(k_TESTER_STEP,"tester_step");
        }else if(mode==i_MODE_OPTIMIZER){
         _KernelCreate(k_ARRAY_FILL,"array_fill");
         _KernelCreate(k_TESTER_OPT_PREPARE,"tester_opt_prepare");
         _KernelCreate(k_TESTER_OPT_STEP,"tester_opt_step");
         _KernelCreate(k_FIND_PATTERNS_OPT,"find_patterns_opt");
        }
      else
         break;
      //--- erstellen der Puffer
      if(!ocl.SetBuffersCount(OCL_BUFFERS_COUNT))
        {
         SET_UERR(UERR_SET_BUF_COUNT,"Failed to create buffers");
         break;
        }
      //--- abrufen der Größe des RAM          
      long gpu_mem_size;
      if(ocl.GetGlobalMemorySize(gpu_mem_size)==false)
        {
         SET_UERR(UERR_GET_MEMORY_SIZE,"Failed to receive RAM value");
         break;
        }
      m_stat.gpu_mem_size.Set(gpu_mem_size);
      m_stat.gpu_mem_usage.Max(gpu_mem_size);
      return true;
     }
   Deinit();
   return false;
  }

Das Argument mode setzt den Initialisierungsmodus. Dies kann eine Optimierung oder ein Einzeltest sein. Darauf aufbauend werden verschiedene Kernel erstellt.

Kernel- und Pufferenumeratoren werden in der Datei OCLInc.mqh deklariert. Die Quellcodes der Kernel werden dort als Ressource angehängt, wie z.B. die Zeichenkette cl_tester.

Die Methode Deinit() löscht OpenCL-Programme und -Objekte:

void COpenCLx::Deinit()
  {
   if(ocl!=NULL)
     {
      //--- löschen der OpenCL-Objekte
      ocl.Shutdown();
      delete ocl;
      ocl=NULL;
     }
  }

Nachdem alle Vorkehrungen erfüllt sind, ist es an der Zeit, mit der Hauptarbeit zu beginnen. Wir haben bereits einen relativ kompakten Code und umfassende Informationen über Fehler.

Aber zuerst müssen wir die Daten hochladen, mit denen wir arbeiten sollen. Das ist nicht so einfach, wie es auf den ersten Blick scheint.


2.1 Laden der Preise

The CBuffering class uploads the data.

class CBuffering
  {
private:
   string            m_symbol;
   ENUM_TIMEFRAMES   m_period;
   int               m_maxbars;
   uint              m_memory_usage;   //Verwendeter Speicher
   bool              m_spread_ena;     //hochladen des Puffers spread
   datetime          m_from;
   datetime          m_to;
   uint              m_timeout;        //Timeout in Millisekunden für das Hochladen
   ulong             m_ts_abort;       //Zeitwert in Mikrosekunden, wenn die Operation abgebrochen werden soll
   //--- erzwungenes Hochladen
   bool              ForceUploading(datetime from,datetime to);
public:
                     CBuffering();
                    ~CBuffering();
   //--- Datenmenge in den Puffern
   int               Depth;
   //--- Puffer
   double            Open[];
   double            High[];
   double            Low[];
   double            Close[];
   double            Spread[];
   datetime          Time[];
   //--- abrufen der zeitlichen Beschränkung für das Hochladen der Daten
   datetime          TimeFrom(void){return m_from;}
   datetime          TimeTo(void){return m_to;}
   //--- 
   int               Copy(string symbol,ENUM_TIMEFRAMES period,datetime from,datetime to,double point=0);
   uint              GetMemoryUsage(void){return m_memory_usage;}
   bool              SpreadBufEnable(void){return m_spread_ena;}
   void              SpreadBufEnable(bool ena){m_spread_ena=ena;}
   void              SetTimeout(uint timeout){m_timeout=timeout;}
  };

Wir werden uns nicht zu sehr damit beschäftigen, da der Daten-Upload keinen direkten Bezug zum aktuellen Thema hat. Wir sollten jedoch seine Anwendung kurz prüfen.

Die Klasse enthält die Puffer Open[], High[], Low[], Close[], Time[] und Spread[]. Wir können mit ihnen arbeiten, nachdem die Methode Copy() erfolgreich funktioniert hat. Vergessen wir nicht, dass der Puffer Spread[] vom Typ 'double' ist und nicht in Points, sondern als eine Preisdifferenz ausgedrückt wird. Außerdem ist das Kopieren des Spread[]-Puffers zunächst deaktiviert. Gegebenenfalls sollte es mit der Methode SpreadBufEnable() aktiviert werden.

Für den Upload wird die Methode Copy() verwendet. Das Argument point wird nur verwendet, um den Spread von Punkten in die Preisdifferenz umzurechnen. Wenn das Kopieren der Spreads ausgeschaltet ist, wird dieses Argument nicht verwendet.

Die Hauptgründe für die Einrichtung einer eigenen Klasse für das Hochladen von Daten sind:

  • Die Unmöglichkeit, Daten in einer Menge herunterzuladen, die TERMINAL_MAXBARS übersteigt, mit der Funktion CopyTime() und dergleichen.
  • Es kann nicht garantiert werden, dass das Terminal die Daten lokal hat.

Die Klasse Cuffering ist in der Lage, große Datenmengen zu kopieren, die über TERMINAL_MAXBARS hinausgehen, sowie den Upload fehlender Daten vom Server auszulösen und auf deren Fertigstellung zu warten. Aufgrund dieses Wartens müssen wir auf die Methode SetTimeout() achten, die zum Einstellen der maximalen Datenladezeit (einschließlich Warten) in Millisekunden bestimmt ist. Standardmäßig ist der Klassenkonstruktor gleich 5000 (5 Sekunden). Wenn Sie den Timeout auf Null setzen, wird er deaktiviert. Dies ist höchst unerwünscht, kann aber in einigen Fällen nützlich sein.

Es gibt jedoch einige Einschränkungen: M1-Periodendaten werden nicht für den Zeitraum von mehr als einem Jahr hochgeladen, was die Reichweite unserer Tester teilweise einschränkt.


2.2 Einzeltest

Der Einzeltest besteht aus den folgenden Punkten:

  1. Herunterladen der Zeitreihenpuffer
  2. Initialisierung von OpenCL
  3. Kopieren der Zeitreihenpuffer in OpenCL-Puffer
  4. Starten des Kernels, der die Muster auf dem aktuellen Chart findet und Ergebnisse in den Orderpuffer als Markteintrittspunkte hinzufügt.
  5. Starten des Kernels, der Aufträge in das M1-Diagramm verschiebt.
  6. Starten des Kernels, der die Handelsergebnisse nach Orders auf Diagramm M1 zählt und sie in den Puffer hinzufügt.
  7. Verarbeitung des Ergebnispuffers und Berechnung der Testergebnisse
  8. Deinitialisierung von OpenCL
  9. Entfernen der Zeitreihenpuffer

Die CBuffering Download-Zeitreihe. Dann sollten diese Daten in die OpenCL-Puffer kopiert werden, damit die Kernel mit ihnen arbeiten können. Diese Aufgabe ist der Methode LoadTimeseriesOCL() zugeordnet. Der Code ist unten aufgeführt:

bool CTestPatterns::LoadTimeseriesOCL()
  {
//--- Open Puffer:
   _BufferFromArray(buf_OPEN,m_sbuf.Open,0,m_sbuf.Depth,CL_MEM_READ_ONLY);
//--- High Puffer:
   _BufferFromArray(buf_HIGH,m_sbuf.High,0,m_sbuf.Depth,CL_MEM_READ_ONLY);
//--- Low Puffer:
   _BufferFromArray(buf_LOW,m_sbuf.Low,0,m_sbuf.Depth,CL_MEM_READ_ONLY);
//--- Close Puffer:
   _BufferFromArray(buf_CLOSE,m_sbuf.Close,0,m_sbuf.Depth,CL_MEM_READ_ONLY);
//--- Time Puffer:
   _BufferFromArray(buf_TIME,m_sbuf.Time,0,m_sbuf.Depth,CL_MEM_READ_ONLY);
//--- Open (M1) Puffer:
   _BufferFromArray(buf_OPEN_M1,m_tbuf.Open,0,m_tbuf.Depth,CL_MEM_READ_ONLY);
//--- High (M1) Puffer:
   _BufferFromArray(buf_HIGH_M1,m_tbuf.High,0,m_tbuf.Depth,CL_MEM_READ_ONLY);
//--- Low (M1) Puffer:
   _BufferFromArray(buf_LOW_M1,m_tbuf.Low,0,m_tbuf.Depth,CL_MEM_READ_ONLY);
//--- Close (M1) Puffer:
   _BufferFromArray(buf_CLOSE_M1,m_tbuf.Close,0,m_tbuf.Depth,CL_MEM_READ_ONLY);
//--- Spread (M1) Puffer:
   _BufferFromArray(buf_SPREAD_M1,m_tbuf.Spread,0,m_tbuf.Depth,CL_MEM_READ_ONLY);
//--- Time (M1) Puffer:
   _BufferFromArray(buf_TIME_M1,m_tbuf.Time,0,m_tbuf.Depth,CL_MEM_READ_ONLY);
//--- kopieren war erfolgreich
   return true;
  }

Die Daten sind also heruntergeladen worden. Jetzt ist es Zeit den Testalgorithmus zu implementieren.


2.2.1 Die Suche nach den Mustern in OpenCL

Der Code für die Muster für OpenCL unterscheidet sich nicht besonders von dem für MQL5:

//--- Muster
#define  PAT_NONE                0
#define  PAT_PINBAR_BEARISH      (1<<0)
#define  PAT_PINBAR_BULLISH      (1<<1)
#define  PAT_ENGULFING_BEARISH   (1<<2)
#define  PAT_ENGULFING_BULLISH   (1<<3)
//--- Preise
#define  O(i) Open[i]
#define  H(i) High[i]
#define  L(i) Low[i]
#define  C(i) Close[i]
//+------------------------------------------------------------------+
//| Prüfen, ob es Muster gibt                                        |
//+------------------------------------------------------------------+
uint Check(__global double *Open,__global double *High,__global double *Low,__global double *Close,double ref,uint flags)
  {
//--- Bearish Pin-Bar  
   if((flags&PAT_PINBAR_BEARISH)!=0)
     {//
      double tail=H(1)-fmax(O(1),C(1));
      if(tail>=ref && C(0)>O(0) && O(2)>C(2) && H(1)>fmax(H(0),H(2)) && fabs(O(1)-C(1))<tail)
         return PAT_PINBAR_BEARISH;
     }
//--- Bullish Pin-Bar  
   if((flags&PAT_PINBAR_BULLISH)!=0)
     {//
      double tail=fmin(O(1),C(1))-L(1);
      if(tail>=ref && O(0)>C(0) && C(2)>O(2) && L(1)<fmin(L(0),L(2)) && fabs(O(1)-C(1))<tail)
         return PAT_PINBAR_BULLISH;
     }
//--- Bearish Engulfing
   if((flags&PAT_ENGULFING_BEARISH)!=0)
     {//
      if((C(1)-O(1))>=ref && H(0)<C(1) && O(2)>C(1) && C(2)<O(1))
         return PAT_ENGULFING_BEARISH;
     }
//--- Bullish Engulfing
   if((flags&PAT_ENGULFING_BULLISH)!=0)
     {//
      if((O(1)-C(1))>=ref && L(0)>C(1) && O(2)<C(1) && C(2)>O(1))
         return PAT_ENGULFING_BULLISH;
     }
//--- nichts gefunden   
   return PAT_NONE;
  }

Einer der kleinen Unterschiede besteht darin, dass die Puffer über einen Zeiger und nicht als Referenz übergeben werden. Außerdem gibt es den Modifikator __global, der anzeigt, dass sich die Zeitreihenpuffer im globalen Speicher befinden. Alle OpenCL-Puffer, die wir erstellen sollen, befinden sich im globalen Speicher.

Die Funktion Check() ruft den Kernel find_patterns() auf:

__kernel void find_patterns(__global double *Open,__global double *High,__global double *Low,__global double *Close,
                            __global int *Order,       // Auftragspuffer
                            __global int *Count,       // Anzahl der Aufträge im Puffer
                            const double ref,          // Parameter der Muster
                            const uint flags)          // Suche nach welchem Muster
  {
//--- arbeiten in einer Dimension  
//--- Barindex  
   size_t x=get_global_id(0);
//--- Größe des Raums der Mustersuche   
   size_t depth=get_global_size(0)-PBARS;
   if(x>=depth)
      return;
//--- prüfen, ob Muster existieren
   uint res=Check(&Open[x],&High[x],&Low[x],&Close[x],ref,flags);
   if(res==PAT_NONE)
      return;
//--- bestimmen der Aufträge
   if(res==PAT_PINBAR_BEARISH || res==PAT_ENGULFING_BEARISH)
     {//verkaufen
      int i=atomic_inc(&Count[0]);
      Order[i*2]=x+PBARS;
      Order[(i*2)+1]=OP_SELL;
     }
   else if(res==PAT_PINBAR_BULLISH || res==PAT_ENGULFING_BULLISH)
     {//kaufen
      int i=atomic_inc(&Count[0]);
      Order[i*2]=x+PBARS;
      Order[(i*2)+1]=OP_BUY;
     }
  }

Wir werden das verwenden, um nach Mustern zu suchen und Aufträge in einem speziell dafür vorgesehenen Puffer zu finden.

Der Kernel find_patterns() arbeitet in einem eindimensionalen Aufgabenbereich. Während des Starts erstellen wir die Anzahl der Arbeitselemente, die wir im Aufgabenraum für die Dimension 0 angeben sollen. In diesem Fall ist es die Anzahl der Balken im aktuellen Zeitrahmen. Um zu verstehen, welche Bar bearbeitet wird, müssen wir den Aufgabenindex abrufen:

size_t x=get_global_id(0);

Wobei Null der Maßindex ist.

Wenn das Ergebnis der Funktionsausführung Check() das Vorhandensein des Musters anzeigt, erteilen wir einen Auftrag im Puffer der aktuellen Periode. Jedee Auftrag soll zwei Zellen belegen, da sie aus dem Barindex in den Puffern der Zeitreihe und der Richtung (Kauf oder Verkauf) besteht. Da alle übergebenen Arrays eindimensional sind, ist es notwendig, die zwei Dimensionen selbst zu implementieren. Wir verwenden die Formel i*2 zum Lokalisieren der Barindizes in Zeitreihen durch gerade Indizes des Ordnungsarrays und die Formel (i*2)+1 zum Lokalisieren von Operationen durch ungerade Indizes, wobei i die Nummer des Auftrags ist:
      Order[i*2]=x+PBARS;
      Order[(i*2)+1]=OP_SELL;

Um die Nummer des Auftrags zu erhalten, verwenden wir die Funktion atomic_inc(). Bei der Ausführung einer Aufgabe haben wir keine Ahnung, welche Aufgaben und Balken bereits erledigt sind. Das sind parallele Berechnungen, und es gibt hier absolut keine Reihenfolge von irgendetwas. Ein Aufgabenindex ist nicht auf die Anzahl der bereits erledigten Aufgaben bezogen. Daher wissen wir nicht, wie viele Aufträge bereits im Puffer liegen. Wenn wir versuchen, ihre Nummer in Zelle 0 des Puffers Count[] zu lesen, kann ein anderer Task gleichzeitig etwas schreiben. Um das zu vermeiden, verwenden wir die Atomicfunktionen.

In unserem Fall deaktiviert die Funktion atomic_inc() den Zugriff anderer Aufgaben auf die Zelle Count[0]. Danach erhöht er seinen Wert um eins, während der vorherige Wert als Ergebnis zurückgegeben wird.

int i=atomic_inc(&Count[0]);

Natürlich verlangsamt dies die Arbeit, da andere Aufgaben warten müssen, solange der Zugriff auf Count[0] gesperrt ist. Aber in einigen Fällen (wie bei uns) gibt es einfach keine andere Lösung.

Nachdem alle Aufgaben erledigt sind, erhalten wir den Puffer mit den erteilen Aufträgen Order[] und deren Anzahl in der Zelle Count[0].


2.2.2 Verschieben der Aufträge auf den Zeitrahmen M1

Also, wir haben Muster im aktuellen Zeitrahmen gefunden, aber die Tests sollten im M1-Zeitrahmen durchgeführt werden. Das bedeutet, dass für alle Einstiegspunkte des aktuellen Zeitrahmens die entsprechenden Bars auf M1 gefunden werden müssen. Da das Handelsmuster auch in kleinen Zeitfenstern eine relativ geringe Anzahl von Einstiegspunkten hat, werden wir eine eher grobe, aber durchaus geeignete Methode wählen — die Enumeration. Wir werden die Zeit jedes gefundenen Auftrags mit der Zeit jeder Bar des Zeitrahmens M1 vergleichen. Dazu erstellen Sie den Kernel order_to_M1():

__kernel void order_to_M1(__global ulong *Time,__global ulong *TimeM1,
                          __global int *Order,__global int *OrderM1,
                          __global int *Count,
                          const ulong shift) // Zeitversatz in Sekunden
  {
//--- Arbeiten in zwei Dimensionen
   size_t x=get_global_id(0); //Index des Zeitindex der Aufträge
   if(OrderM1[x*2]>=0)
      return;
   size_t y=get_global_id(1); //Index in TimeM1
   if((Time[Order[x*2]]+shift)==TimeM1[y])
     {
      atomic_inc(&Count[1]);
      //--- setzen der geraden Indices im Puffer TimeM1
      OrderM1[x*2]=y;
      //--- setzen (OP_BUY/OP_SELL) ungeraden Indices
      OrderM1[(x*2)+1]=Order[(x*2)+1];
     }
  }

Hier haben wir einen zweidimensionalen Aufgabenraum. Die Dimension 0 ist für die Anzahl der erteilten Aufträge, während die Dimension 1 für Anzahl der Bars des Zeitrahmens M1 ist. Wenn die Eröffnungszeit der Bar eines Auftrags und die der Bar in M1 übereinstimmen, wird der Vorgang des aktuellen Auftrags in den Puffer OrderM1[] kopiert und der erfasste Barindex in der Zeitreihe des M1-Zeitrahmens gesetzt.

Es gibt hier zwei Dinge, die auf den ersten Blick nicht existieren sollten.

  • Die erste ist die atomic_inc() Atomicfunktion, die aus irgendeinem Grund die Einstiegspunkte des M1-Zeitrahmens zählt. In der Dimension 0 arbeitet jeder Auftrag mit ihrem Index, während es im Index 1 nicht mehr als eine Übereinstimmung geben kann. Das bedeutet, dass der Versuch des gemeinsamen Zugriffs völlig ausgeschlossen ist. Warum müssen wir dennoch zählen?
  • Das zweite ist das Argument shift, das zur aktuellen Zeit der aktuellen Bar des Zeitrahmens hinzugefügt wurde. 

Dafür gibt es besondere Gründe. Die Welt ist nicht perfekt. Das Vorhandensein einer Bar auf dem M5-Diagramm mit der Öffnungszeit von 01:00:00:00 bedeutet nicht, dass eine Bar mit der gleichen Öffnungszeit auf dem M1-Diagramm vorhanden ist.

Die entsprechende Bar auf dem M1-Diagramm kann eine Öffnungszeit von entweder 01:01:00 oder 01:04:00 haben. Mit anderen Worten, die Anzahl der Variationen entspricht dem Verhältnis der Dauer der Zeitrahmen. Dazu wird die Funktion des Zählens der Anzahl der erfassten Eintrittspunkte für M1 eingeführt:

atomic_inc(&Count[1]);

Wenn nach Abschluss des Kerneloperationen die Anzahl der gefundenen M1-Aufträge gleich der Anzahl der im aktuellen Zeitrahmen erfassten Aufträge ist, dann ist die Aufgabe vollständig abgeschlossen. Andernfalls ist ein Neustart mit einem anderen Argument shift erforderlich. Es kann so viele Neustarts geben, wie es M1-Perioden im aktuellen Zeitrahmen gibt

Die folgende Prüfung wurde eingeführt, um sicherzustellen, dass erkannte Einstiegspunkte während eines Neustarts mit einem Argumentwert ungleich Null shift nicht durch andere Werte neu geschrieben werden:

   if(OrderM1[x*2]>=0)
      return;

Damit es funktioniert, füllen wir den Puffer OrderM1[] mit dem Wert -1, bevor wir den Kernel starten. Wir erstellen dazu den Kernel array_fill():

__kernel void array_fill(__global int *Buf,const int value)
  {
//--- arbeitet mit einer Dimension    
   size_t x=get_global_id(0);
   Buf[x]=value;
  }


2.2.3 Erhalt der Handelsergebnisse

Nachdem die Einstiegspunkte auf M1 gefunden wurden, können wir beginnen, Handelsergebnisse zu ermitteln. Dazu benötigen wir einen Kernel, der offene Positionen begleitet. Mit anderen Worten, wir sollten aus einem der vier Gründe warten, bis sie geschlossen sind:

  • Take-Profit erreicht,
  • Stop-Loss erreicht,
  • Ablauf der maximalen Haltezeit der offenen Position erreicht,
  • Ende des Testzeitraums erreicht.

Die Aufgabe für den Kernel ist eindimensional und ihre Größe entspricht der Anzahl der Aufträge. Der Kernel soll über die Balken iterieren, beginnend mit der geöffneten Position und die oben beschriebenen Bedingungen überprüfen. Innerhalb der Bar werden die Ticks im Modus "1 Minute OHLC" simuliert, wie im Abschnitt "Testing Trading Strategies" der Dokumentation beschrieben.

Wichtig ist, dass einige Positionen fast unmittelbar nach dem Öffnen geschlossen werden, einige später, während andere durch Timeout oder nach Beendigung des Tests geschlossen werden. Dies bedeutet, dass die Ausführungszeit der Aufgabe für verschiedene Einstiegspunkte sehr unterschiedlich ist.

Die Praxis hat gezeigt, dass das Begleiten einer Position vor dem Schließen in einem Durchgang nicht effizient ist. Im Gegensatz dazu führt die Aufteilung des Prüfraums (die Anzahl der Bars vor dem Zzwangsweisen Schließen der Position wegen des Erreichens der Haltezeit) in mehrere Teile und die Durchführung des Handlings in mehreren Durchgängen zu deutlich besseren Ergebnissen in Bezug auf die Leistung.

Die Aufgaben, die im aktuellen Durchgang nicht erledigt sind, werden auf den nächsten verschoben. Somit wird die Größe des Aufgabenbereichs mit jedem Durchlauf verringert. Um dies zu implementieren, müssen Sie jedoch einen anderen Puffer verwenden, um die Task-Indizes zu speichern. Jede Aufgabe ist ein Index eines Einstiegspunktes im Auftragspuffer. Zum Zeitpunkt des ersten Starts entspricht der Inhalt des Aufgabenpuffers vollständig dem Auftragspuffer. Bei den nächsten Starts wird es die Indizes der Aufträge enthalten, für die noch keine Positionen geschlossen wurden. Um mit dem Aufgabenpuffer zu arbeiten und die Aufgaben für den nächsten Lauf dort gleichzeitig zu speichern, sollte es zwei Bänke haben: Eine Bank wird beim aktuellen Start verwendet, während eine andere für die Bildung von Aufgaben für den nächsten.

In der Praxis sieht das so aus. Angenommen, wir haben 1000 Einstiegspunkte, für die wir Handelsergebnisse ermitteln müssen. Die Haltezeit einer offenen Position entspricht 800 Bars. Wir haben uns entschieden, den Test in 4 Durchläufe aufzuteilen. Grafisch sieht es so aus, wie in Abb. 7 dargestellt.


Abb. 7. Begleiten der offenen Positionen in mehreren Durchläufen


Durch Versuch und Irrtum haben wir die optimale Anzahl von Durchläufen von 8 für eine Positionshaltezeit von 12 Stunden (oder 720 Minutenbars) ermittelt. Dies ist der Standardwert. Er variiert für verschiedene Timeout-Werte und OpenCL-Geräte. Für maximale Leistung wird eine sorgfältige Auswahl empfohlen.

Somit werden die Puffer der Tasks[] und der Index der Bank der Tasks, mit denen wir arbeiten, zu den Argumenten des Kernels neben den Zeitreihen hinzugefügt. Außerdem fügen wir den Puffer Res[] hinzu, um die Ergebnisse zu speichern.

Die Menge der Ist-Daten im Task-Puffer wird über den Puffer Left[] zurückgegeben, der die Größe der beiden Elemente hat — jeweils für jede der Banken.

Da der Test in Teilen durchgeführt wird, sollten die Werte der Start- und Endbars für die Positionsverfolgung zwischen den Kernelargumenten übergeben werden. Dies ist ein relativer Wert, der mit dem Index der positionell öffnenden Balken aufsummiert wird, um den absoluten Index der aktuellen Bars in der Zeitreihe zu erhalten. Außerdem sollte der maximal zulässige Index der Bars in der Zeitreihe an den Kernel übergeben werden, um die Pufferlänge nicht zu überschreiten.

Dadurch sehen die Kernelargumente von tester_step(), der die offene Positionen verfolgen soll, wie folgt aus:

__kernel void tester_step(__global double *OpenM1,__global double *HighM1,__global double *LowM1,__global double *CloseM1,
                          __global double *SpreadM1, // Differenz in Preisen, nicht in Points
                          __global ulong *TimeM1,
                          __global int *OrderM1,     // Auftragspuffer, wobei [0] den Index von OHLC(M1), [1] - (kaufen/verkaufen) enthält
                          __global int *Tasks,       // Aufgabenpuffer (der offenen Positionen) speichert die Indices der Aufträge im Puffer OrderM1
                          __global int *Left,        // Anzahl der verbliebenen Aufgaben, zwei Elemente: [0] - für bank0, [1] - für bank1
                          __global double *Res,      // Ergebnispuffer
                          const uint bank,           // laufende Bank                           
                          const uint orders,         // Auftragsanzahl in OrderM1
                          const uint start_bar,      // Seriennummer der bearbeiteten Bar (asl Abstand zum angegebenen Index in OrderM1)
                          const uint stop_bar,       // die letzte zu bearbeitenden Bar
                          const uint maxbar,         // maximal akzeptabler Barindex (die letzte Bar des Arrays)
                          const double tp_dP,        // TP als Preisdifferenz
                          const double sl_dP,        // SL als Preisdifferenz
                          const ulong timeout)       // für das zwangsweise Schließen einer Position (in Sekunden) 

Der Kernel tester_step() arbeitet in zwei Dimensionen. Die Größe der Dimension der Aufgaben ändert sich bei jedem Aufruf, die mit der Nummer des Auftrags beginnt und sich mit jedem Durchlauf vermindert.

Wir erhalten eine Arbeits-ID (task ID) beim Start des Kernels:

   size_t id=get_global_id(0);

Dann, basierend auf dem Index der aktuellen Bank, der über das Argument bank übergeben wird, berechnen wir den Index wie folgt:

   uint bank_next=(bank)?0:1;

Wir berechnen den Index des Auftrags, mit der wir arbeiten sollen. Beim ersten Start (wenn start_bar gleich Null ist) entspricht der Task-Puffer dem Auftragspuffer, d.h. der Auftragsindex ist gleich dem Arbeitsindex. Bei den nachfolgenden Starts wird der Auftragsindex aus dem Aufgabenpuffer unter Berücksichtigung der aktuellen Bank und des Arbeitsindex ermittelt:

   if(!start_bar)
      idx=id;
   else
      idx=Tasks[(orders*bank)+id];

Da wir den Auftragsindex kennen, erhalten wir den Index der Bar in der Zeitreihe und die Arbeitsnummer:

//--- Index der Bar der Positionseröffnung im Puffer M1
   uint iO=OrderM1[idx*2];
//--- (OP_BUY/OP_SELL) Aktion
   uint op=OrderM1[(idx*2)+1];

Auf Basis des Wertes des Arguments timeout, wird die Zeit berechnet für ein erzwungenes Schließen der Position:

   ulong tclose=TimeM1[iO]+timeout;

Dann wird die offene Position bearbeitet. Betrachten wir beispielsweise den Umgang mit KAUF-Positionen (mit VERKAUFS-Positionen wird ähnlich verfahren).

   if(op==OP_BUY)
     {
      //--- Eröffnungspreis der Position
      double open=OpenM1[iO]+SpreadM1[iO];
      double tp = open+tp_dP;
      double sl = open-sl_dP;
      double p=0;
      for(uint j=iO+start_bar; j<=(iO+stop_bar); j++)
        {
         for(uint k=0;k<4;k++)
           {
            if(k==0)
              {
               p=OpenM1[j];
               if(j>=maxbar || TimeM1[j]>=tclose)
                 {
                  //--- erzwungenes Schließen wegen der Zeit
                  Res[idx]=p-open;
                  return;
                 }
              }
            else if(k==1)
               p=HighM1[j];
            else if(k==2)
               p=LowM1[j];
            else
               p=CloseM1[j];
            //--- prüfen, on TP oder SL ausgelöst wurden
            if(p<=sl)
              {
               Res[idx]=sl-open;
               return;
              }
            else if(p>=tp)
              {
               Res[idx]=tp-open;
               return;
              }
           }
        }
     }

Wenn keine der Bedingung für das Verlassen des Kernels erfüllt sind, wird die Aufgabe auf den nächsten Durchlauf verschoben:

   uint i=atomic_inc(&Left[bank_next]);
   Tasks[(orders*bank_next)+i]=idx;

Nachdem alle Durchläufe bearbeitet wurden, speichert der Puffer Res[] die Ergebnisse aller Positionen. Um das Testergebnis zu erhalten, müssten diese zusammengefasst werden.

Nachdem der Algorithmus klar ist und die Kernel bereit sind, sollten wir damit beginnen, zu starten.


2.3 Starten eines Tests

Die Klasse CTestPatterns wird uns dabei helfen:

class CTestPatterns : private COpenCLx
  {
private:
   CBuffering       *m_sbuf;  // Zeitreihen des aktuellen Zeitrahmens
   CBuffering       *m_tbuf;  // Zeitreihen des M1-Zeitrahmens 
   int               m_prepare_passes;
   uint              m_tester_passes;
   bool              LoadTimeseries(datetime from,datetime to);
   bool              LoadTimeseriesOCL(void);
   bool              test(STR_TEST_STAT &stat,datetime from,datetime to,STR_TEST_PARS &par);
   bool              optimize(STR_TEST_STAT &stat,datetime from,datetime to,STR_OPT_PARS &par);
   void              buffers_free(void);
public:
                     CTestPatterns();
                    ~CTestPatterns();
   //--- starten eines Einzeltests                    
   bool              Test(STR_TEST_STAT &stat,datetime from,datetime to,STR_TEST_PARS &par);
   //--- starten der Optimierung   
   bool              Optimize(STR_TEST_STAT &stat,datetime from,datetime to,STR_OPT_PARS &par);
   //--- abrufen der Pointer auf das Programm der Statistik   
   COCLStat         *GetStat(void){return &m_stat;}
   //--- Abfrage des letzten fehlers   
   int               GetLastError(void){return m_last_error.code;}
   //--- abrufen der Struktur des letzten Fehlers
   STR_ERROR         GetLastErrorExt(void){return m_last_error;}
   //--- Rücksetzen des letzten Fehlers  
   void              ResetLastError(void);
   //--- Anzahl der Durchläufe, in die der Start des Testkerns unterteilt ist.
   void              SetTesterPasses(uint tp){m_tester_passes=tp;}
   //--- Anzahl der Durchläufe, in die der Start des Kernels der Auftragsvorbereitung unterteilt ist.
   void              SetPrepPasses(int p){m_prepare_passes=p;}
  };

Betrachten wir die Methode Test() im Detail:

bool CTestPatterns::Test(STR_TEST_RESULT &result,datetime from,datetime to,STR_TEST_PARS &par)
  {
   ResetLastError();
   m_stat.Reset();
   m_stat.time_total.Start();
//--- hochladen der Daten der Zeitreihen   
   m_stat.time_buffering.Start();
   if(LoadTimeseries(from,to)==false)
      return false;
   m_stat.time_buffering.Stop();
//--- initialisieren von OpenCL
   m_stat.time_ocl_init.Start();
   if(Init(i_MODE_TESTER)==false)
      return false;
   m_stat.time_ocl_init.Stop();
//--- Teststart
   bool result=test(stat,from,to,par);
   Deinit();
   buffers_free();
   m_stat.time_total.Stop();
   return result;
  }

Beim Start gibt es eine Zeitspanne, in der die Strategie getestet werden soll, sowie die Links zur Struktur der Parameter und der Testergebnisse.

Wenn erfolgreich, gibt die Methode 'true' zurück und schreibt die Ergebnisse in das Argument "result". Wenn während der Ausführung ein Fehler aufgetreten ist, gibt die Methode 'false' zurück. Um Fehlerdetails zu erhalten, rufen wir GetLastErrorExt() auf.

Zuerst laden wir die Daten der Zeitreihe hoch. Dann initialisieren wir OpenCL. Dazu gehört auch das Erstellen von Objekten und der Kernel. Wenn alles in Ordnung ist, rufen wir die Methode test() auf, die den gesamten Testalgorithmus enthält. Tatsächlich ist die Methode Test() ein Wrapper für test(). Dies geschieht, um sicherzustellen, dass die Deinitialisierung nach jedem Ende "test"-Methode durchgeführt wird und Zeitreihenpuffer freigegeben werden.

In der Methode test() beginnt alles mit dem Hochladen von Zeitreihenpuffern in die OpenCL-Puffer:
   if(LoadTimeseriesOCL () ==false)returnfalse;

Die geschieht mit der oben bereits beschriebenen Methode LoadTimeseriesOCL().

Der Kernel find_patterns(), der mit der Enumeration k_FIND_PATTERNS korrespondiert, wird zuerst gestartet. Vor dem Start, sollten wir die Puffer der Aufträge und der Ergebnisse erstellen:

   _BufferCreate(buf_ORDER,m_sbuf.Depth*2*sizeof(int),CL_MEM_READ_WRITE);
   int  count[2]={0,0};
   _BufferFromArray(buf_COUNT,count,0,2,CL_MEM_READ_WRITE);

Die Größe des Auftrags-Puffers ist doppelt so groß wie die des aktuellen Zeitrahmens. Da wir nicht genau wissen, wieviele Muster gefunden werden, wird angenommen, es gäbe ein Muster auf jeder Bar. Diese Annahme mag auf den ersten Blick absurd erscheinen. Im Weiteren aber, wenn zusätzliche Muster hinzugefügt werden, würde uns das viele Probleme ersparen.

Bestimmen der Argumente:

   _SetArgumentBuffer(k_FIND_PATTERNS,0,buf_OPEN);
   _SetArgumentBuffer(k_FIND_PATTERNS,1,buf_HIGH);
   _SetArgumentBuffer(k_FIND_PATTERNS,2,buf_LOW);
   _SetArgumentBuffer(k_FIND_PATTERNS,3,buf_CLOSE);
   _SetArgumentBuffer(k_FIND_PATTERNS,4,buf_ORDER);
   _SetArgumentBuffer(k_FIND_PATTERNS,5,buf_COUNT);
   _SetArgument(k_FIND_PATTERNS,6,double(par.ref)*_Point);
   _SetArgument(k_FIND_PATTERNS,7,par.flags);

Für den Kernel find_patterns() bestimmen wir einen eindimensionalen Arbeitsraum mit einem Offset von Null:

   uint global_size[1];
   global_size[0]=m_sbuf.Depth;
   uint work_offset[1]={0};

Wir beginnen die Ausführung des Kernels find_patterns().

   _Execute(k_FIND_PATTERNS,1,work_offset,global_size);
Es ist zu beachten, dass das Beenden der Methode Execute() nicht bedeutet, dass das Programm ausgeführt wird. Es kann noch ausgeführt oder zur Ausführung in die Warteschlange gestellt werden. Um den aktuellen Status zu ermitteln, verwenden wir die Funktion CLExecutionStatus(). Wenn wir auf den Abschluss des Programms warten müssen, können wir seinen Status regelmäßig überwachen oder den Puffer lesen, in den das Programm die Ergebnisse stellt. Im zweiten Fall erfolgt das Warten auf die Beendigung des Programms in der Methode BufferRead() zum Lesen der Puffer.

   _BufferRead(buf_COUNT,count,0,0,2);

Jetzt können wir beim Index 0 des Puffers Count[] die Anzahl der erkannten Muster oder die Anzahl der Aufträge im entsprechenden Puffer finden. Der nächste Schritt besteht darin, die entsprechenden Einstiegspunkte im M1-Zeitrahmen zu finden. Der Kernel order_to_M1() summiert die erfasste Menge in den gleichen Puffer count[], allerdings bei Index 1. Das Auslösen der Bedingung (count[0]==count[1]) gilt als erfolgreich.

Aber zuerst müssen wir den Puffer der Aufträge für M1 erstellen und ihn mit dem Wert -1 füllen. Da wir die Anzahl der Aufträge bereits kennen, geben wir die genaue Größe des Puffers an:

   int len=count[0]*2;
   _BufferCreate(buf_ORDER_M1,len*sizeof(int),CL_MEM_READ_WRITE);

Wir setzen die Argumente für den Kernel array_fill():

   _SetArgumentBuffer(k_ARRAY_FILL,0,buf_ORDER_M1);
   _SetArgument(k_ARRAY_FILL,1,int(-1));

Wir setzen den eindimensionalen Aufgabenraum mit dem Anfangswert Null und der Größe des Puffers Start der Ausführung:

   uint opt_init_work_size[1];
   opt_init_work_size[0]=len;
   uint opt_init_work_offset[1]={0};
   _Execute(k_ARRAY_FILL,1,opt_init_work_offset,opt_init_work_size);

Als nächstes sollten wir die Ausführung des Kernels order_to_M1() starten:

//--- bestimmen der Argumente
   _SetArgumentBuffer(k_ORDER_TO_M1,0,buf_TIME);
   _SetArgumentBuffer(k_ORDER_TO_M1,1,buf_TIME_M1);
   _SetArgumentBuffer(k_ORDER_TO_M1,2,buf_ORDER);
   _SetArgumentBuffer(k_ORDER_TO_M1,3,buf_ORDER_M1);
   _SetArgumentBuffer(k_ORDER_TO_M1,4,buf_COUNT);
//--- der Aufgabenraum für den Kernel k_ORDER_TO_M1 ist zweidimensional
   uint global_work_size[2];
//--- die erste Dimension enthält die übriggebliebenen Aufträge des Kernels k_FIND_PATTERNS
   global_work_size[0]=count[0];
//--- die zweite Dimension enthält alle M1-Bars
   global_work_size[1]=m_tbuf.Depth;
//--- der anfängliche Offset im Aufgabenraum für beide Dimensionen ist gleich Null
   uint global_work_offset[2]={0,0};

Das Argument mit dem Index 5 wird nicht gesetzt, da sein Wert unterschiedlich ist und es unmittelbar vor dem Start der Kernelausführung gesetzt wird. Aus dem oben genannten Grund kann die Ausführung des Kernels order_to_M1() mehrmals mit unterschiedlichem Offsetwert in Sekunden ausgeführt werden. Die maximale Anzahl der Starts wird durch das Verhältnis der Dauer der aktuellen und M1-Diagramme begrenzt:

   int maxshift=PeriodSeconds()/PeriodSeconds(PERIOD_M1);

Die ganze Schleife sieht so aus:

   for(int s=0;s<maxshift;s++)
     {
      //--- setzen des Offset des aktuellen Durchlaufs
      _SetArgument(k_ORDER_TO_M1,5,ulong(s*60));
      //--- ausführen des Kernels
      _Execute(k_ORDER_TO_M1,2,global_work_offset,global_work_size);
      //--- lesen der Ergebnisse
      _BufferRead(buf_COUNT,count,0,0,2);
      //--- auf dem Index 0, befinden sich die Anzahl der Aufträge auf dem aktuellen Chart
      //--- auf dem Index 1, die Anzahl der entsprechenden Bars im M1-Chart
      //--- wenn beide Werte übereinstimmen, wird der Schleife verlassen
      if(count[0]==count[1])
         break;
      //--- andernfalls wird mit der nächsten Iteration fortgesetzt und der Kernel mit einem anderen Offset gestartet
     }
//--- nochmal prüfen, ob die Anzahl der Aufträge stimmt, falls die Schleife eben nicht mittels 'break' beendet wurde
   if(count[0]!=count[1])
     {
      SET_UERRt(UERR_ORDERS_PREPARE,"M1 orders preparation error");
      return false;
     }

Jetzt ist es an der Zeit, den Kernel tester_step() zu starten, der die Ergebnisse der Positionen berechnet, die von erkannten Einstiegspunkten geöffnet wurden. Zuerst erstellen wir die fehlenden Puffer und setzen die Argumente:

//--- erstellen des Aufgabenpuffers, wobei die Anzahl der Aufgaben des nächsten Durchlaufs ermittelt wird
   _BufferCreate(buf_TASKS,m_sbuf.Depth*2*sizeof(int),CL_MEM_READ_WRITE);
//--- erstellen des Ergebnispuffers, dem die Ergebnisse zugewiesen werden
   _BufferCreate(buf_RESULT,m_sbuf.Depth*sizeof(double),CL_MEM_READ_WRITE);
//--- setzen der Argumente für den Kernel eines Einzeltests   
   _SetArgumentBuffer(k_TESTER_STEP,0,buf_OPEN_M1);
   _SetArgumentBuffer(k_TESTER_STEP,1,buf_HIGH_M1);
   _SetArgumentBuffer(k_TESTER_STEP,2,buf_LOW_M1);
   _SetArgumentBuffer(k_TESTER_STEP,3,buf_CLOSE_M1);
   _SetArgumentBuffer(k_TESTER_STEP,4,buf_SPREAD_M1);
   _SetArgumentBuffer(k_TESTER_STEP,5,buf_TIME_M1);
   _SetArgumentBuffer(k_TESTER_STEP,6,buf_ORDER_M1);
   _SetArgumentBuffer(k_TESTER_STEP,7,buf_TASKS);
   _SetArgumentBuffer(k_TESTER_STEP,8,buf_COUNT);
   _SetArgumentBuffer(k_TESTER_STEP,9,buf_RESULT);
   uint orders_count=count[0];
   _SetArgument(k_TESTER_STEP,11,uint(orders_count));
   _SetArgument(k_TESTER_STEP,14,uint(m_tbuf.Depth-1));
   _SetArgument(k_TESTER_STEP,15, double(par.tp)*_Point);
   _SetArgument(k_TESTER_STEP,16, double(par.sl)*_Point);
   _SetArgument(k_TESTER_STEP,17,ulong(par.timeout));

Als Nächstes konvertieren wir die maximale Haltezeit in die Anzahl der Bars auf M1:

   uint maxdepth=(par.timeout/PeriodSeconds(PERIOD_M1))+1;

Dann prüfen wir, ob die angegebene Anzahl der Ausführungsdurchläufe der Kernel gültig ist. Standardmäßig sind es 8, aber um die optimale Leistung für verschiedene OpenCL-Geräte zu definieren, ist es erlaubt, andere Werte mit der Methode SetTesterPasses() einzustellen.

   if(m_frames_counter<1)
      m_tester_passes=1;
   if(m_tester_passes>maxdepth)
      m_tester_passes=maxdepth;
   uint step_size=maxdepth/m_tester_passes;

Wir setzen die Größe des Arbeitsraums für eine einzelne Dimension und starten die Schleife zur Berechnung der Handelsergebnisse:

   global_size[0]=orders_count;
   m_stat.time_ocl_test.Start();
   for(uint i=0;i<m_tester_passes;i++)
     {
      //--- setzen des Index der laufenden Bank
      _SetArgument(k_TESTER_STEP,10,uint(i&0x01));
      uint start_bar=i*step_size;
      //--- setzen des Index der Bar, mit dem der aktuelle Durchlauf startet
      _SetArgument(k_TESTER_STEP,12,start_bar);
      //--- setzen des Index der Bar, mit dem der Test während des laufenden Durchlaufs durchgeführt werden soll
      uint stop_bar=(i==(m_tester_passes-1))?(m_tbuf.Depth-1):(start_bar+step_size-1);
      _SetArgument(k_TESTER_STEP,13,stop_bar);
      //--- rücksetzen der Anzahl der Aufgaben in der nächsten Bank 
      //--- damit soll die Anzahl der noch übrigen Aufträge für den nächsten Durchlauf gesichert werden
      count[(~i)&0x01]=0;
      _BufferWrite(buf_COUNT,count,0,0,2);
      //--- starten des Testkernels
      _Execute(k_TESTER_STEP,1,work_offset,global_size);
      //--- lesen der Anzahl der verbliebenen Aufträge für den nächsten Durchlauf
      _BufferRead(buf_COUNT,count,0,0,2);
      //--- setzt die neue Zahl an Aufgaben gleich der Anzahl an Aufträgen
      global_size[0]=count[(~i)&0x01];
      //--- wenn keine Aufgaben mehr existieren, Schleife verlassen
      if(!global_size[0])
         break;
     }
   m_stat.time_ocl_test.Stop();

Wir erstellen den Puffer zum Lesen der Handelsergebnisse:

   double Result[];
   ArrayResize(Result,orders_count);
   _BufferRead(buf_RESULT,Result,0,0,orders_count);

Um vergleichbare Ergebnisse zu denen des integrierten Testers zu erhalten, sollten die gelesenen Werte in _Point umgewandelt werden. Der Code zur Berechnung der Ergebnisse und Statistiken ist unten aufgeführt:

   m_stat.time_proc.Start();
   result.trades_total=0;
   result.gross_loss=0;
   result.gross_profit=0;
   result.net_profit=0;
   result.loss_trades=0;
   result.profit_trades=0;
   for(uint i=0;i<orders_count;i++)
     {
      double r=Result[i]/_Point;
      if(r>=0)
        {
         result.gross_profit+=r;
         result.profit_trades++;
           }else{
         result.gross_loss+=r;
         result.loss_trades++;
        }
     }
   result.trades_total=result.loss_trades+result.profit_trades;
   result.net_profit=result.gross_profit+result.gross_loss;
   m_stat.time_proc.Stop();

Schreiben wir noch ein Skript, das es uns erlaubt, unseren Tester zu starten.

#include <OCL_Patterns\TestPatternsOCL.mqh>

CTestPatterns tpat;
//+------------------------------------------------------------------+
//| Script Programm Start Funktion                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   datetime from=D'2018.01.01 00:00';
   datetime to=D'2018.10.01 00:00';
//--- setzen der Testparameter
   STR_TEST_PARS pars;
   pars.ref= 60;
   pars.sl = 350;
   pars.tp = 50;
   pars.flags=15;  // alle Muster
   pars.timeout=12*3600;
//--- Ergebnisstruktur
   STR_TEST_RESULT res;
//--- Teststart
   tpat.Test(res,from,to,pars);
   STR_ERROR oclerr=tpat.GetLastErrorExt();
   if(oclerr.code)
     {
      Print(oclerr.comment);
      Print("code = ",oclerr.code,", function = ",oclerr.function,", line = ",oclerr.line);
      return;
     }
//--- Testergebnisse  
   Print("Net Profit: ",   res.net_profit);
   Print("Gross Profit: ", res.gross_profit);
   Print("Gross Loss: ",   res.gross_loss);
   Print("Trades Total: ", res.trades_total);
   Print("Profit Trades: ",res.profit_trades);
   Print("Loss Trades: ",  res.loss_trades);
//--- Ausführungsstatistik
   COCLStat ocl_stat=tpat.GetStat();
   Print("GPU memory size: ",       ocl_stat.gpu_mem_size.ToStr());
   Print("GPU memory usage: ",      ocl_stat.gpu_mem_usage.ToStr());
   Print("Buffering: ",             ocl_stat.time_buffering.ToStr());
   Print("OpenCL init: ",           ocl_stat.time_ocl_init.ToStr());
   Print("OpenCL buffering: ",      ocl_stat.time_ocl_buf.ToStr());
   Print("OpenCL prepare orders: ", ocl_stat.time_ocl_orders.ToStr());
   Print("OpenCL test: ",           ocl_stat.time_ocl_test.ToStr());
   Print("OpenCL total execution: ",ocl_stat.time_ocl_exec.ToStr());
   Print("Post-processing: ",       ocl_stat.time_proc.ToStr());
   Print("Total: ",                 ocl_stat.time_total.ToStr());
  }

Die verwendete Zeitspanne des Tests, das Symbol und der Zeitrahmen sind diejenigen, mit denen wir bereits den in MQL5 implementierten EA getestet haben. Die verwendeten Werte von Referenz und Stop-Loss sind diejenigen, die bei der Optimierung gefunden wurden. Jetzt müssen wir nur noch das Skript ausführen und das erhaltene Ergebnis mit dem des integrierten Testers vergleichen.

Abb. 8. Die Ergebnisse des in OpenCL implementierten Testers


Somit ist die Anzahl der Positionen gleich, während der Nettogewinn nicht gleich ist. Der integrierte Tester zeigt einen Wert von 1000,50 an, während unser Tester 1007,99 ausgibt. Der Grund dafür ist wie folgt. Um die gleichen Ergebnisse zu erzielen, müssten wir unter anderem die Swaps berücksichtigen. Aber die Implementierung in unseren Tester ist nicht gerechtfertigt. Für eine grobe Schätzung, bei der der Modus "1 Minute OHLC" angewendet wird, können solche Kleinigkeiten vernachlässigt werden. Wichtig ist, dass die Ergebnisse fast gleich sind, was bedeutet, dass unser Algorithmus korrekt funktioniert.

Werfen wir nun einen Blick auf die Statistik der Programmausführung. Es wurden nur 16 MB Speicherplatz benötigt. Die Initialisierung von OpenCL hat am meisten Zeit in Anspruch genommen. Der gesamte Prozess dauerte 376 Millisekunden, was dem integrierten Tester sehr ähnlich ist. Es ist sinnlos, hier einen Performancegewinn zu erwarten. Bei 200 Positionen werden wir mehr Zeit mit Vorbereitungsarbeiten wie Initialisierung, Kopieren von Puffern und so weiter verbringen. Um den Unterschied zu spüren, brauchen wir hundertmal mehr Aufträge für den Test. Es ist an der Zeit, zur Optimierung überzugehen.


2.4. Optimierung

Der Optimierungsalgorithmus soll dem einzelnen Testalgorithmus mit einem grundlegenden Unterschied ähnlich sein. Während wir im Tester nach Mustern suchen und dann die Handelsergebnisse zählen, ist hier die Reihenfolge der Aktionen unterschiedlich. Zuerst zählen wir die Handelsergebnisse und beginnen danach mit der Suche nach Mustern. Der Grund dafür ist, dass wir zwei optimierte Parameter haben. Der erste ist ein Referenzwert für das Auffinden der Muster. Der zweite ist Stop-Loss, der für die Berechnung des Handelsergebnisses verwendet wird. So beeinflusst einer von ihnen die Anzahl der Einstiegspunkte, während der andere Einfluss auf die Handelsergebnisse und die Dauer der Verfolgung der offenen Positionen hat. Wenn wir die gleiche Reihenfolge der Aktionen wie im einzelnen Testalgorithmus beibehalten, werden wir nicht vermeiden, dass ein erneuter Test der gleichen Einstiegspunkte zu einem enormen Zeitverlust führt, da sich die Pin-Bar mit einem "tail" von 300 Points bei jedem Referenzwert befindet, der gleich oder kleiner als dieser Wert ist.

Daher ist es in unserem Fall viel sinnvoller, die Ergebnisse der Positionen mit Einstiegspunkten auf jeder Bar (einschließlich Kauf und Verkauf) zu berechnen und dann mit diesen Daten während der Mustersuche zu arbeiten. Die Reihenfolge der Aktionen bei der Optimierung sieht also wie folgt aus:

  1. Herunterladen der Zeitreihenpuffer
  2. Initialisierung von OpenCL
  3. Kopieren der Zeitreihenpuffer in OpenCL-Puffer
  4. Starten der Vorbereitung des Auftragskernels (zwei Aufträge - Kauf und Verkauf - für jede Bar des aktuellen Zeitrahmens)
  5. Starten des Kernels, der Aufträge in das M1-Diagramm verschiebt.
  6. Starten des Kernels, der die Handelsergebnisse mittels der Aufträge zählt
  7. Starten des Kernels, der die Muster findet und die Testergebnisse für jede Kombination der optimierten Parameter der fertigen Handelsergebnisse findet
  8. Bearbeiten der Ergebnispuffer und nach den optimierten Parametern sucht, die zum besten Ergebnis führen
  9. Deinitialisierung von OpenCL
  10. Entfernen der Zeitreihenpuffer

Darüber hinaus wird die Anzahl der Aufgaben zum Durchsuchen von Mustern mit der Anzahl der Werte des Referenzwerts multipliziert und die Anzahl der Aufgaben zum Berechnen der Ergebnisse von Transaktionen wird mit der Anzahl der Werte der Stop-Loss-Stufe multipliziert.

2.4.1 Vorbereitung der Aufträge

Wir gehen davon aus, dass die gewünschten Muster auf jeder Bar zu finden sein könnten. Das bedeutet, dass wir für jede Bar eine Kauf- oder Verkaufsorder platzieren müssten. Die Puffergröße kann durch die folgende Gleichung definiert werden:

N = Depth*4*SL_count;

wobei Depth eine Größe der Puffer der Zeitreihen ist, während SL_count eine Anzahl von Stop-Loss Werten ist.

Außerdem sollten die Indizes der Bars aus der M1-Zeitreihe stammen. Der Kernel tester_opt_prepare() durchsucht die Zeitreihe nach M1 Bars mit der Öffnungszeit, die der Öffnungszeit der aktuellen Zeitrahmen entspricht, und platziert sie im oben angegebenen Format im Auftragspuffer. Im Allgemeinen ist seine Arbeit ähnlich wie die des Kernels order_to_M1():

__kernel void tester_opt_prepare(__global ulong *Time,__global ulong *TimeM1,
                                 __global int *OrderM1,// Auftragspuffer
                                 __global int *Count,
                                 const int   SL_count,      // Anzahl von SL
                                 const ulong shift)         // Zeitversatz in Sekunden
  {
//--- Arbeiten in zwei Dimensionen   
   size_t x=get_global_id(0); //Index in Time
   if(OrderM1[x*SL_count*4]>=0)
      return;
   size_t y=get_global_id(1); //Index in TimeM1
   if((Time[x]+shift)==TimeM1[y])
     {
      //--- finden des höchsten Barindex für M1
      atomic_max(&Count[1],y);
      uint offset=x*SL_count*4;
      for(int i=0;i<SL_count;i++)
        {
         uint idx=offset+i*4;
         //--- zwei Aufträge (Kauf- und Verkauf) für jede Bar hinzufügen
         OrderM1[idx++]=y;
         OrderM1[idx++]=OP_BUY |(i<<2);
         OrderM1[idx++]=y;
         OrderM1[idx]  =OP_SELL|(i<<2);
        }
      atomic_inc(&Count[0]);
     }
  }

Es gibt jedoch einen wichtigen Unterschied — das Finden des maximalen Index der M1-Zeitreihe. Lassen Sie mich erklären, warum dies geschehen ist.

Bei der Prüfung eines einzelnen Durchlaufs haben wir es mit einer relativ geringen Anzahl von Aufträgen zu tun. Die Anzahl der Aufgaben, die der Anzahl der Aufträge multipliziert mit der Größe der Puffer der M1-Zeitreihe entspricht, ist ebenfalls relativ gering. Wenn wir die Daten betrachten, mit denen wir den Test durchgeführt haben, sind das 200 Aufträge multipliziert mit 279 039 М1-Bars, die letztlich 55,8 Millionen Aufgaben liefern.

In der aktuellen Situation wird die Anzahl der Aufgaben viel größer sein. Dies sind beispielsweise 279 039 M1-Bars multipliziert mit 55 843 Bars der aktuellen Periode (M5), was 15,6 Milliarden Aufgaben entspricht. Es lohnt sich auch zu bedenken, dass man diesen Kernel mit einem anderen Wert für Time-Shift erneut ausführen kann. Die Enumeratiosmethode ist hier zu ressourcenintensiv.

Um dieses Problem zu lösen, lassen wir die Aufzählung unverändert, obwohl wir den Handhabungsbereich der aktuellen Periodenbalken in mehrere Teile unterteilen. Außerdem sollten wir den Bereich der geeigneten Minutenbars begrenzen. Da jedoch der berechnete Indexwert des oberen Randes der Minutenbalken in den meisten Fällen den tatsächlichen übersteigt, geben wir nach Count[1] den maximalen Index eines Minutenbalkens zurück, um den nächsten Durchgang von diesem Punkt aus zu starten.


2.4.2 Erhalt der Handelsergebnisse

Nach der Vorbereitung der Aufträge ist es an der Zeit, Handelsergebnisse abzurufen.

Der Kernel Tester_opt_step() ist dem Tester_step() sehr ähnlich. Daher werde ich nicht den gesamten Code zur Verfügung stellen, sondern mich auf die Unterschiede konzentrieren. Zuerst haben sich die Eingaben geändert:

__kernel void tester_opt_step(__global double *OpenM1,__global double *HighM1,__global double *LowM1,__global double *CloseM1,
                              __global double *SpreadM1,// in Preisdifferenzen, nicht in Points
                              __global ulong *TimeM1,
                              __global int *OrderM1,     // Auftragspuffer, wobei [0] den Index von OHLC(M1), [1] - (kaufen/verkaufen) enthält
                              __global int *Tasks,       // Aufgabenpuffer (der offenen Positionen) speichert die Indices der Aufträge im Puffer OrderM1
                              __global int *Left,        // Anzahl der verbliebenen Aufgaben, zwei Elemente: [0] - für bank0, [1] - für bank1
                              __global double *Res,      // Ergebnispuffe, Zuweisung erfolgt gleich nach Erhalt, 
                              const uint bank,           // der laufenden bank                           
                              const uint orders,         // Auftragsanzahl in OrderM1
                              const uint start_bar,      // Seriennummer der bearbeiteten Bar (asl Abstand zum angegebenen Index in OrderM1) - tatsächlich kommt "i" aus der Schleife, die den Kernel startet
                              const uint stop_bar,       // die letzte zu bearbeitende Bar - meist gleich 'bar'
                              const uint maxbar,         // maximal akzeptabler Barindex (die letzte Bar des Arrays)
                              const double tp_dP,        // TP als Preisdifferenz
                              const uint sl_start,       // SL in Points - Anfangswert
                              const uint sl_step,        // SL in Points - Schrittweite
                              const ulong timeout,       // Laufzeit der Positionen (in Sekunden), danach werden sie zwangsweise geschlossen
                              const double point)        // _Point

Anstelle des Arguments sl_dP, mit dem der SL-Level-Wert, ausgedrückt in Preisdifferenz, übergeben wird, haben wir nun zwei Argumente: sl_start und sl_step sowie das Argument 'point'. Nun sollte die folgende Gleichung angewendet werden, um den SL-Pegelwert zu berechnen:

SL = (sl_start+sl_step*sli)*point;

wobei sli ein Wert des im Auftrag enthaltenen Stop-Loss Index ist.

Der zweite Unterschied ist ein Code zum Empfangen des Indexes sli aus dem Auftragspuffer:

//--- Aktion (bits 1:0) und SL-Index (bits 9:2)
   uint opsl=OrderM1[(idx*2)+1];
//--- SL-Index abrufen   
   uint sli=opsl>>2;

Der Rest des Codes ist identisch mit Tester_step().

Nach der Ausführung erhalten wir Kauf- und Verkaufsergebnisse für jede Bar und jeden Stop-Loss im Res[] Puffer.


2.4.3 Die Suche nach den Mustern und die Formung der Testergebnisse

Im Gegensatz zum Testen fassen wir hier die Ergebnisse der Positionen direkt im Kernel zusammen, nicht im MQL-Code. Es gibt jedoch einen unangenehmen Nachteil — wir müssen die Ergebnisse in einen ganzzahligen Typ umwandeln, was zwangsläufig zu einem Verlust an Genauigkeit führt. Daher sollten wir im Argument point den Wert _Point dividiert durch 100 übergeben.

Die erzwungene Umwandlung der Ergebnisse in den Typ 'int' ist darauf zurückzuführen, dass die Atomicfunktionen nicht mit dem Typ 'double' umgehen können. atomic_add() ist für die Summierung der Ergebnisse zu verwenden.

Der Kernel find_patterns_opt() soll im dreidimensionalen Aufgabenraum arbeiten:

  • Dimension 0: Barindex des aktuellen Zeitrahmens
  • Dimension 1: Referenzwertindex der Muster
  • Dimension 2: Stop-Loss Index

Im Laufe der Arbeit wird ein Ergebnispuffer erzeugt. Der Puffer enthält Teststatistiken für jede Kombination aus Stop-Loss und Referenzwert. Die Teststatistik ist eine Struktur, die die folgenden Werte enthält:

  • Gesamtgewinn
  • Gesamtverlust
  • Anzahl der Positionen mit Gewinn
  • Anzahl der Positionen mit Verlust

Alle sind vom Typ 'int'. Mit denen kann dann der Nettogewinn und die Gesamtzahl aller Positionen ermittelt werden. Der Code des Kernels ist im folgenden zu sehen:

__kernel void find_patterns_opt(__global double *Open,__global double *High,__global double *Low,__global double *Close,
                                __global double *Test,     // Puffer der Testergebnisse von jeder Bar, die Größe ist 2*x*z ([0]-buy, [1]-sell ... )
                                __global int *Results,     // Ergebnispuffer, Größe: 4*y*z 
                                const double ref_start,    // Parameter der Muster
                                const double ref_step,     // 
                                const uint flags,          // welche Muster sollen gesucht werden
                                const double point)        // _Point/100
  {
//--- arbeitet wmit drei Dimensionen
//--- Barindex 
   size_t x=get_global_id(0);
//--- Ref.-Wert-Index
   size_t y=get_global_id(1);
//--- SL-Index
   size_t z=get_global_id(2);
//--- Anzahl der Bars
   size_t x_sz=get_global_size(0);
//--- Anzahl der Ref.-Werte
   size_t y_sz=get_global_size(1);
//--- Anzahl der SL-Werte
   size_t z_sz=get_global_size(2);
//--- Größe des Raums der Mustersuche   
   size_t depth=x_sz-PBARS;
   if(x>=depth)//nahe am Pufferende keine neue Position eröffnen
      return;
//
   uint res=Check(&Open[x],&High[x],&Low[x],&Close[x],ref_start+ref_step*y,flags);
   if(res==PAT_NONE)
      return;
//--- berechnen des Indes des Handelsergebnisses für den Puffer Test[]
   int ri;
   if(res==PAT_PINBAR_BEARISH || res==PAT_ENGULFING_BEARISH) //verkaufen
      ri = (x+PBARS)*z_sz*2+z*2+1;
   else                                                      //kaufen
      ri=(x+PBARS)*z_sz*2+z*2;
//--- abrufen des Ergebnisses des errechneten Index und konvertieren in Cents
   int r=Test[ri]/point;
//--- berechnen des Index des Testergebnisses im Puffer Results[]
   int idx=z*y_sz*4+y*4;
//--- hinzufügen des Handelsergebnisses zum aktuellen Muster
   if(r>=0)
     {//--- Gewinn
      //--- summieren des Gesamtgewinns in Cent
      atomic_add(&Results[idx],r);
      //--- Erhöhen der Anzahl der Positionen mit Gewinn
      atomic_inc(&Results[idx+2]);
     }
   else
     {//--- Verlust
      //--- summieren des Gesamtverlustes in Cent
      atomic_add(&Results[idx+1],r);
      //---  Erhöhen der Anzahl der Positionen mit Verlust
      atomic_inc(&Results[idx+3]);
     }
  }

Der Puffer Test[] in den Argumenten enthält die Ergebnisse nach der Ausführung des Kernels tester_opt_step().


2.5 Starten der Optimierung

Der Code zum Starten der Kernel aus MQL5 während der Optimierung ist ähnlich aufgebaut wie der Testprozess. Die öffentliche Methode Optimize() ist ein Wrapper der Methode optimize(), mit der die Reihenfolge der Vorbereitung und des Starts der Kernel festgelegt wird.

bool CTestPatterns::Optimize(STR_TEST_RESULT &result,datetime from,datetime to,STR_OPT_PARS &par)
  {
   ResetLastError();
   if(par.sl.step<=0 || par.sl.stop<par.sl.start || 
      par.ref.step<=0 || par.ref.stop<par.ref.start)
     {
      SET_UERR(UERR_OPT_PARS,"Optimization parameters are incorrect");
      return false;
     }
   m_stat.Reset();
   m_stat.time_total.Start();
//--- hochladen der Daten der Zeitreihen   
   m_stat.time_buffering.Start();
   if(LoadTimeseries(from,to)==false)
      return false;
   m_stat.time_buffering.Stop();
//--- initialisieren von OpenCL
   m_stat.time_ocl_init.Start();
   if(Init(i_MODE_OPTIMIZER)==false)
      return false;
   m_stat.time_ocl_init.Stop();
//--- starten der Optimierung
   bool res=optimize(result,from,to,par);
   Deinit();
   buffers_free();
   m_stat.time_total.Stop();
   return res;
  }

Wir werden nicht jede Zeile im Detail betrachten. Konzentrieren wir uns nur auf die Unterschiede, insbesondere den Start des Kernels tester_opt_prepare().

Erstellen wir zunächst den Puffer für die Verwaltung der Anzahl der bearbeiteten Bars und geben den maximalen Index der M1-Bars zurück:

   int count[2]={0,0};
   _BufferFromArray(buf_COUNT,count,0,2,CL_MEM_READ_WRITE);

Dann bestimmen wir die Argumente und die Größe der Arbeitsraum.

   _SetArgumentBuffer(k_TESTER_OPT_PREPARE,0,buf_TIME);
   _SetArgumentBuffer(k_TESTER_OPT_PREPARE,1,buf_TIME_M1);
   _SetArgumentBuffer(k_TESTER_OPT_PREPARE,2,buf_ORDER_M1);
   _SetArgumentBuffer(k_TESTER_OPT_PREPARE,3,buf_COUNT);
   _SetArgument(k_TESTER_OPT_PREPARE,4,int(slc)); // Anzahl von SL
//--- der Kernel k_TESTER_OPT_PREPARE hat einen zweidimensionalen Aufgabenraum
   uint global_work_size[2];
//--- Dimension 0 - Aufträge des aktuellen Zeitrahmens 
   global_work_size[0]=m_sbuf.Depth;
//--- Dimension 1 - alle Bars von M1   
   global_work_size[1]=m_tbuf.Depth;
//--- für den ersten Start wird der Offset im Aufgabenraum für beide Dimensionen auf Null setzen.   
   uint global_work_offset[2]={0,0};

Der Offset der 1. Dimension im Arbeitsraum wird nach der Bearbeitung eines Teils der Bars vergrößert. Sein Wert soll gleich dem Maximalwert von М1 Bar sein, d.h. den um 1 erhöhten Kernel zurückgeben.

   int maxshift=PeriodSeconds()/PeriodSeconds(PERIOD_M1);
   int prep_step=m_sbuf.Depth/m_prepare_passes;
   for(int p=0;p<m_prepare_passes;p++)
     {
      //Offset des aktuellen Zeitrahmens im Aufgabenraum space
      global_work_offset[0]=p*prep_step;
      //Offset des Zeitrahmens M1 im Aufgabenraum
      global_work_offset[1]=count[1];
      //Aufgabengröße des aktuellen Zeitrahmens
      global_work_size[0]=(p<(m_prepare_passes-1))?prep_step:(m_sbuf.Depth-global_work_offset[0]);
      //Aufgabengröße des M1-Zeitrahmens
      uint sz=maxshift*global_work_size[0];
      uint sz_max=m_tbuf.Depth-global_work_offset[1];
      global_work_size[1]=(sz>sz_max)?sz_max:sz;
      //
      count[0]=0;
      _BufferWrite(buf_COUNT,count,0,0,2);
      for(int s=0;s<maxshift;s++)
        {
         _SetArgument(k_TESTER_OPT_PREPARE,5,ulong(s*60));
         //--- Kernelausführung
         _Execute(k_TESTER_OPT_PREPARE,2,global_work_offset,global_work_size);
         //--- abrufen der Ergebnisse (die Zahl sollte mit m_sbuf.Depth übereinstimmen)
         _BufferRead(buf_COUNT,count,0,0,2);
         if(count[0]==global_work_size[0])
            break;
        }
      count[1]++;
     }
   if(count[0]!=global_work_size[0])
     {
      SET_UERRt(UERR_ORDERS_PREPARE,"Failed to prepare M1 orders");
      return false;
     }

Der Parameter m_prepare_passes gibt an, in wie viele Durchläufe die Auftragsvorbereitung unterteilt werden soll. Standardmäßig ist sein Wert 64, allerdings kann er mit der Methode SetPrepPasses() geändert werden.

Nach dem Lesen der Testergebnisse im Puffer OptResults[] wird nach der Kombination optimierter Parameter gesucht, die zu einem maximalen Nettogewinn führen.

   int max_profit=-2147483648;
   uint idx_ref_best= 0;
   uint idx_sl_best = 0;
   for(uint i=0;i<refc;i++)
      for(uint j=0;j<slc;j++)
        {
         uint idx=j*refc*4+i*4;
         int profit=OptResults[idx]+OptResults[idx+1];
         //sum+=profit;
         if(max_profit<profit)
           {
            max_profit=profit;
            idx_ref_best= i;
            idx_sl_best = j;
           }
        }

Danach werden die Ergebnisse in 'double' umgerechnet und die gewünschten Werte der optimierten Parameter in der entsprechenden Struktur zugewiesen.

   uint idx=idx_sl_best*refc*4+idx_ref_best*4;
   result.gross_profit=double(OptResults[idx])/100;
   result.gross_loss=double(OptResults[idx+1])/100;
   result.profit_trades=OptResults[idx+2];
   result.loss_trades=OptResults[idx+3];
   result.trades_total=result.loss_trades+result.profit_trades;
   result.net_profit=result.gross_profit+result.gross_loss;
//---
   par.ref.value= int(par.ref.start+idx_ref_best*par.ref.step);
   par.sl.value = int(par.sl.start+idx_sl_best*par.sl.step);

Beachten Sie, dass die Umwandlung von 'int' in 'double' und umgekehrt die Ergebnisse sicherlich beeinflusst, so dass sie sich leicht von denen unterscheiden, die während des Einzeltests erzielt wurden.

Schreiben wir ein kleines Skript, um die Optimierung zu starten:

#include <OCL_Patterns\TestPatternsOCL.mqh>

CTestPatterns tpat;
//+------------------------------------------------------------------+
//| Script Programm Start Funktion                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   datetime from=D'2018.01.01 00:00';
   datetime to=D'2018.10.01 00:00';
//--- setzen der Optimierungsparameter
   STR_OPT_PARS optpar;
   optpar.ref.start = 15;
   optpar.ref.step  = 5;
   optpar.ref.stop  = 510;
   optpar.sl.start = 15;
   optpar.sl.step  = 5;
   optpar.sl.stop  = 510;
   optpar.flags=15;
   optpar.tp=50;
   optpar.timeout=12*3600;
//--- Ergebnisstruktur
   STR_TEST_RESULT res;
//--- starten der Optimierung
   tpat.Optimize(res,from,to,optpar);
   STR_ERROR oclerr=tpat.GetLastErrorExt();
   if(oclerr.code)
     {
      Print(oclerr.comment);
      Print("code = ",oclerr.code,", function = ",oclerr.function,", line = ",oclerr.line);
      return;
     }
//--- Werte der optimierten Parameter
   Print("Ref: ",optpar.ref.value,", SL: ",optpar.sl.value);
//--- Testergebnisse  
   Print("Net Profit: ",   res.net_profit);
   Print("Gross Profit: ", res.gross_profit);
   Print("Gross Loss: ",   res.gross_loss);
   Print("Trades Total: ", res.trades_total);
   Print("Profit Trades: ",res.profit_trades);
   Print("Loss Trades: ",  res.loss_trades);
//--- Ausführungsstatistik
   COCLStat ocl_stat=tpat.GetStat();
   Print("GPU memory size: ",       ocl_stat.gpu_mem_size.ToStr());
   Print("GPU memory usage: ",      ocl_stat.gpu_mem_usage.ToStr());
   Print("Buffering: ",             ocl_stat.time_buffering.ToStr());
   Print("OpenCL init: ",           ocl_stat.time_ocl_init.ToStr());
   Print("OpenCL buffering: ",      ocl_stat.time_ocl_buf.ToStr());
   Print("OpenCL prepare orders: ", ocl_stat.time_ocl_orders.ToStr());
   Print("OpenCL test: ",           ocl_stat.time_ocl_test.ToStr());
   Print("OpenCL total execution: ",ocl_stat.time_ocl_exec.ToStr());
   Print("Post-processing: ",       ocl_stat.time_proc.ToStr());
   Print("Total: ",                 ocl_stat.time_total.ToStr());
  }

Die Eingabeparameter sind die gleichen, die wir für die Optimierung mit dem integrierten Tester verwende. Starten wir:

Abb. 9. Optimierung mit OpenCL

Wie man sieht, stimmen die Ergebnisse mit denen des integrierten Testers nicht überein. Warum? Könnte der Verlust der Genauigkeit bei der Umwandlung von 'double' in 'int' und umgekehrt dabei eine entscheidende Rolle spielen? Theoretisch könnte dies der Fall sein, wenn sich die Ergebniszahlen nach dem Komma unterscheiden. Aber die Unterschiede sind signifikant.

Der eingebaute Tester zeigt Ref = 60 und SL = 350 mit dem Nettogewinn von 1000,50. Der OpenCL-Tester zeigt Ref = 60 und SL = 365 mit dem Nettogewinn von 1506,40. Versuchen wir, einen normalen Tester mit den Werten des OpenCL-Testers auszuführen:


Abb. 10. Überprüfen der Optimierungsergebnisse des OpenCL-Testers

Das Ergebnis ist unserem sehr ähnlich. Das ist also nicht der Verlust der Genauigkeit. Der genetische Algorithmus hat diese Kombination von optimierten Parametern übersprungen. Starten wir den integrierten Tester im Modus der langsamen Optimierung mit dem Test aller Parameter.


Abb. 11. Starten des integrierten Strategietesters im Modus der langsamen Optimierung

Wie wir sehen können, findet der integrierte Tester bei einer Optimierung mit allen Parametern die gleichen Sollwerte Ref = 60 und SL = 365 wie der OpenCL-Tester. Das bedeutet, dass der von uns implementierte Optimierungsalgorithmus korrekt funktioniert.


3. Leistungsvergleich

Jetzt ist es an der Zeit, die Leistung des integrierten mit der des OpenCL-Testers zu vergleichen.

Wir werden den Zeitaufwand für die Optimierung der Parameter der oben beschriebenen Strategie vergleichen. Der integrierte Tester soll in zwei Modi gestartet werden: schnell (genetischer Algorithmus, ausgewählte Parameterkombinationen) und langsam optimiert (alle Parameterkombinationen). Der Start erfolgt auf einem PC mit den folgenden Eigenschaften:

Betriebssystem
Windows 10 (build 17134) x64
CPU
AMD FX-8300 Eight-Core Processor, 3600MHz
RAM
24 574 Mb
Typ der Medien, auf denen der MetaTrader installiert wurde
HDD

6 von 8 Kernen sind den Testagenten zugeordnet.

Der OpenCL-Tester soll auf AMD Radeon HD 7950 Video 3Gb RAM und 800Mhz GPU-Frequenz gestartet werden.

Die Optimierung soll mit drei Paaren durchgeführt werden: EURUSD, GBPUSD und USDJPY. Bei jedem Paar wird die Optimierung in vier Zeitbereichen für jeden Optimierungsmodus durchgeführt. Wir werden die folgenden Abkürzungen verwenden:

 Optimierungsmodus
Beschreibung
Tester Fast
Integrierter Strategietester, genetischer Algorithmus
Tester Slow
Integrierter Strategietester, es werden alle Parameterkombinationen getestet
Tester OpenCL
Tester, der OpenCL verwendet

Ausersehene Testzeiträume:

Zeiträume
Zeitspannen
1 month
2018.09.01 - 2018.10.01
3 Monate
2018.07.01 - 2018.10.01
6 Monate
2018.04.01 - 2018.10.01
9 Monate
2018.01.01 - 2018.10.01

Das für uns wichtigste Ergebnis sind die Werte der gesuchten Parameter, den Nettogewinn, die Anzahl der Positionen und der Zeitbedarf für die Optimierung.


3.1. Optimization on EURUSD

H1, 1 Monat:

Ergebnis
Tester Fast
Tester Slow
Tester OpenCL
Reference
15
15
15
Stop-Loss
330
510
500
Gesamt netto Profit
942.5
954.8
909.59
Anzahl der Positionen
48
48
47
Dauer der Optimierung
10 Sek.
6 Min. 2 Sek.
405.8 ms

H1, 3 Monate:

 Ergebnis Tester Fast 
 Tester Slow Tester OpenCL 
Reference 50
65
70
Stop-Loss 250
235
235
Gesamt netto Profit 1233.8
1503.8
1428.35
Anzahl der Positionen 110
89
76
Dauer der Optimierung 9 Sek.
8 Min. 8 Sek.
457.9 ms

H1, 6 Monate:

 Ergebnis  Tester Fast Tester Slow 
 Tester OpenCL
Reference 15
20
20
Stop-Loss 455
435
435
Gesamt netto Profit 1641.9
1981.9
1977.42
Anzahl der Positionen 325
318
317
Dauer der Optimierung 15 Sec.
11 Min. 13 Sek.
405.5 ms

H1, 9 Monate:

 Ergebnis Tester Fast 
Tester Slow 
 Tester OpenCL
Reference 15
15
15
Stop-Loss 440
435
435
Gesamt netto Profit 1162.0
1313.7
1715.77
Anzahl der Positionen 521
521
520
Dauer der Optimierung 20 Sek.
16 Min. 44 Sek.
438.4 ms

H1, 1 Monat:

Ergebnis
Tester Fast
Tester Slow
Tester OpenCL
Reference
135
45
45
Stop-Loss
270
205
205
Gesamt netto Profit
47
417
419.67
Anzahl der Positionen
1
39
39
Dauer der Optimierung
7 Sek.
9 Min. 27 Sek.
418 ms

H1, 3 Monate:

 Ergebnis Tester Fast 
 Tester Slow Tester OpenCL 
Reference 120
70
70
Stop-Loss 440
405
405
Gesamt netto Profit 147
342
344.85
Anzahl der Positionen 3
16
16
Dauer der Optimierung 11 Sek.
8 Min. 25 Sek.
585.9 ms

H1, 6 Monate:

 Ergebnis  Tester Fast Tester Slow 
 Tester OpenCL
Reference 85
70
70
Stop-Loss 440
470
470
Gesamt netto Profit 607
787
739.6
Anzahl der Positionen 22
47
46
Dauer der Optimierung 21 Sek.
12 Min. 03 Sek.
796.3 ms

H1, 9 Monate:

 Ergebnis Tester Fast 
Tester Slow 
 Tester OpenCL
Reference 60
60
60
Stop-Loss 495
365
365
Gesamt netto Profit 1343.7
1500.5
1506.4
Anzahl der Positionen 200
200 200
Dauer der Optimierung 20 Sek.
16 Min. 44 Sek.
438.4 ms


3.2. Optimierung auf GBPUSD

H1, 1 Monat:

Ergebnis
Tester Fast
Tester Slow
Tester OpenCL
Reference
175
90
90
Stop-Loss
435
185
185
Gesamt netto Profit
143.40
173.4
179.91
Anzahl der Positionen
3
13
13
Dauer der Optimierung
10 Sek.
4 Min. 33 Sek.
385.1 ms

H1, 3 Monate:

 Ergebnis Tester Fast 
 Tester Slow Tester OpenCL 
Reference 175
145
145
Stop-Loss 225
335
335
Gesamt netto Profit 93.40
427
435.84
Anzahl der Positionen 13
19
19
Dauer der Optimierung 12 Sek.
7 Min. 37 Sek.
364.5 ms

H1, 6 Monate:

 Ergebnis  Tester Fast Tester Slow 
 Tester OpenCL
Reference 165
170
165
Stop-Loss 230
335 335
Gesamt netto Profit 797.40
841.2
904.72
Anzahl der Positionen 31
31
32
Dauer der Optimierung 18 Sek.
11 Min. 3 Sek.
403.6 ms

H1, 9 Monate:

 Ergebnis Tester Fast 
Tester Slow 
 Tester OpenCL
Reference 165
165 165
Stop-Loss 380
245
245
Gesamt netto Profit 1303.8
1441.6
1503.33
Anzahl der Positionen 74
74
75
Dauer der Optimierung 24 Sek.
19 Min. 23 Sek.
428.5 ms

H1, 1 Monat:

Ergebnis
Tester Fast
Tester Slow
Tester OpenCL
Reference
335
45
45
Stop-Loss
450
485
485
Gesamt netto Profit
50
484.6
538.15
Anzahl der Positionen
1
104
105
Dauer der Optimierung
12 Sek.
9 Min. 42 Sek.
412.8 ms

H1, 3 Monate:

 Ergebnis Tester Fast 
 Tester Slow Tester OpenCL 
Reference 450 105
105
Stop-Loss 440 240
240
Gesamt netto Profit 0
220
219.88
Anzahl der Positionen 0
16
16
Dauer der Optimierung 15 Sec.
8 Min. 17 Sek.
552.6 ms

H1, 6 Monate:

 Ergebnis  Tester Fast Tester Slow 
 Tester OpenCL
Reference 510
105
105
Stop-Loss 420
260
260
Gesamt netto Profit 0
220
219.82
Anzahl der Positionen 0
23
23
Dauer der Optimierung 24 Sek.
14 Min. 58 Sek.
796.5 ms

H1, 9 Monate:

 Ergebnis Tester Fast 
Tester Slow 
 Tester OpenCL
Reference 185
195
185
Stop-Loss 205
160
160
Gesamt netto Profit 195
240
239.92
Anzahl der Positionen 9
9
9
Dauer der Optimierung 25 Sek.
20 Min. 58 Sek.
4.4 ms


3.3. Optimierung auf USDJPY

H1, 1 Monat:

Ergebnis
Tester Fast
Tester Slow
Tester OpenCL
Reference
60
50
50
Stop-Loss
425
510
315
Gesamt netto Profit
658.19
700.14
833.81
Anzahl der Positionen
18
24
24
Dauer der Optimierung
6 Sek.
4 Min. 33 Sek.
387.2 ms

H1, 3 Monate:

 Ergebnis Tester Fast 
 Tester Slow Tester OpenCL 
Reference 75
55
55
Stop-Loss 510
510
460
Gesamt netto Profit 970.99
1433.95
1642.38
Anzahl der Positionen 50
82
82
Dauer der Optimierung 10 Sek.
6 Min. 32 Sek.
369 ms

H1, 6 Monate:

 Ergebnis  Tester Fast Tester Slow 
 Tester OpenCL
Reference 150
150
150
Stop-Loss 345
330
330
Gesamt netto Profit 273.35
287.14
319.88
Anzahl der Positionen 14
14
14
Dauer der Optimierung 17 Sek.
11 Min. 25 Sek.
409.2 ms

H1, 9 Monate:

 Ergebnis Tester Fast 
Tester Slow 
 Tester OpenCL
Reference 190
190
190
Stop-Loss 425
510
485
Gesamt netto Profit 244.51
693.86
755.84
Anzahl der Positionen 16
16
16
Dauer der Optimierung 24 Sek.
17 Min. 47 Sek.
445.3 ms

H1, 1 Monat:

Ergebnis
Tester Fast
Tester Slow
Tester OpenCL
Reference
30
35
35
Stop-Loss
225
100
100
Gesamt netto Profit
373.60
623.73
699.79
Anzahl der Positionen
53
35
35
Dauer der Optimierung
7 Sek.
4 Min. 34 Sek.
415.4 ms

H1, 3 Monate:

 Ergebnis Tester Fast 
 Tester Slow Tester OpenCL 
Reference 45
40
40
Stop-Loss 335
250
250
Gesamt netto Profit 1199.34
1960.96
2181.21
Anzahl der Positionen 71
99
99
Dauer der Optimierung 12 Sek.
8 Min.
607.2 ms

H1, 6 Monate:

 Ergebnis  Tester Fast Tester Slow 
 Tester OpenCL
Reference 130
40
40
Stop-Loss 400
130
130
Gesamt netto Profit 181.12
1733.9
1908.77
Anzahl der Positionen 4
229
229
Dauer der Optimierung 19 Sek.
12 Min. 31 Sek.
844 ms

H1, 9 Monate:

 Ergebnis Tester Fast 
Tester Slow 
 Tester OpenCL
Reference 35
30
30
Stop-Loss 460
500
500
Gesamt netto Profit 3701.30
5612.16
6094.31
Anzahl der Positionen 681
1091
1091
Dauer der Optimierung 34 Sek.
18 Min. 56 Sek.
1 Sek.


3.4. Tabelle der Leistungsergebnisse

Die erzielten Ergebnisse zeigen, dass der integrierte Tester im schnellen Optimierungsmodus (genetischer Algorithmus) oft die besten Ergebnisse überspringt. Daher ist es fairer, die Leistung bei einer Optimierung aller Parametermöglichkeiten mit der von OpenCL zu vergleichen. Für mehr Transparenz sollten wir eine Übersichtstabelle über die Zeit, die für die Optimierung aufgewendet wird, erstellen.

 Optimierungsbedingungen
 Tester Slow
Tester OpenCL
Verhältnis
EURUSD, H1, 1 Monat:
6 Min. 2 Sek.
405.8 ms
891
EURUSD, H1, 3 Monat:
8 Min. 8 Sek.
457.9 ms
1065
EURUSD, H1, 6 Monat:
11 Min. 13 Sek.
405.5 ms
1657
EURUSD, H1, 9 Monat:
16 Min. 44 Sek.
438.4 ms
2292
EURUSD, H1, 1 Monat:
9 Min. 27 Sek.
418 ms
1356
EURUSD, H1, 3 Monat:
8 Min. 25 Sek.
585.9 ms
861
EURUSD, H1, 6 Monat:
12 Min. 3 Sek.
796.3 ms
908
EURUSD, H1, 9 Monat:
17 Min. 39 Sek.
1 Sek.
1059
GBPUSD, H1, 1 Monat: 4 Min. 33 Sek.
385.1 ms
708
GBPUSD, H1, 3 Monat: 7 Min. 37 Sek.
364.5 ms
1253
GBPUSD, H1, 6 Monat: 11 Min. 3 Sek.
403.6 ms
1642
GBPUSD, H1, 9 Monat: 19 Min. 23 Sek.
428.5 ms
2714
GBPUSD, H1, 1 Monat: 9 Min. 42 Sek.
412.8 ms
1409
GBPUSD, H1, 3 Monat: 8 Min. 17 Sek.
552.6 ms
899
GBPUSD, H1, 6 Monat: 14 Min. 58 Sek.
796.4 ms
1127
GBPUSD, H1, 9 Monat: 20 Min. 58 Sek.
970.4 ms
1296
GBPUSD, H1, 1 Monat: 4 Min. 33 Sek.
387.2 ms
705
GBPUSD, H1, 3 Monat: 6 Min. 32 Sek.
369 ms
1062
USDJPY, H1, 6 Monat: 11 Min. 25 Sek.
409.2 ms
1673
USDJPY, H1, 9 Monat: 17 Min. 47 Sek.
455.3 ms
2396
USDJPY, M5, 1 Monat: 4 Min. 34 Sek.
415.4 ms
659
USDJPY, H1, 3 Monat: 8 Min.
607.2 ms
790
USDJPY, M5, 6 Monat: 12 Min. 31 Sek.
844 ms
889
USDJPY, M5, 9 Monat: 18 Min. 56 Sek.
1 Sek.
1136
Wie wir in der Tabelle sehen können, ist in einigen Fällen der OpenCL-Tester in der Lage, Ergebnisse 2714 mal schneller zu erzielen als der integrierte. Dies ist ein guter Geschwindigkeitsvorteil für diejenigen, die ihre Zeit schätzen.


Schlussfolgerung

In diesem Artikel haben wir den Algorithmus zum Aufbau eines Testers für die einfachste Handelsstrategie mit OpenCL implementiert. Natürlich ist diese Implementierung nur eine der möglichen Lösungen, und sie hat viele Nachteile. Unter ihnen sind:

  • Arbeiten im Modus "1 Minute OHLC", geeignet nur für grobe Schätzungen.
  • Keine Berücksichtigungen von Swaps und Provisionen
  • Falsches Arbeiten mit Umtauschraten
  • Kein Trailing-Stop
  • Keine Berücksichtigung der Anzahl der gleichzeitig geöffneten Positionen
  • Kein Nachteil bei der Rückgabe von Parametern

Trotzdem kann der Algorithmus sehr hilfreich sein, wenn es darum geht, die Leistung der einfachsten Muster schnell und grob zu bewerten, da er es Ihnen ermöglicht, dies tausendmal schneller zu bewältigen als der integrierte Tester, wenn er mit der langsamen, vollständigen Optimierung läuft, und zehnmal schneller als der Tester mit dem genetischen Algorithmus.


Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/4236

Beigefügte Dateien |
MQL5.zip (26.93 KB)
Entwicklung eines Symbolauswahl- und Navigationsprogramms in MQL5 und MQL4 Entwicklung eines Symbolauswahl- und Navigationsprogramms in MQL5 und MQL4
Erfahrene Händler sind sich der Tatsache bewusst, dass die meisten zeitaufwendigen Dinge im Handel nicht das Öffnen und Verfolgen von Positionen sind, sondern das Auswählen von Symbolen und das Suchen von Einstiegspunkten. In diesem Artikel werden wir einen EA entwickeln, das die Suche nach Einstiegspunkten für Handelsinstrumente Ihres Brokers vereinfacht.
Die eigene, multi-threaded, asynchrone Web-Anfrage in MQL5 Die eigene, multi-threaded, asynchrone Web-Anfrage in MQL5
Der Artikel beschreibt die Bibliothek, mit der Sie die Effizienz von HTTP-Anfragen mit WebRequest in MQL5 erhöhen können. Die Ausführung von WebRequest im nicht-blockierenden Modus verwendet in zusätzliche Threads, die Hilfscharts und Expert Advisors verwendet, um nutzerdefinierte Ereignisse austauschen und gemeinsame Ressourcen lesen. Die Quellcodes sind ebenfalls besprochen und beigefügt.
Die Wahrscheinlichkeitstheorie für den Handel von Kurslücken verwenden Die Wahrscheinlichkeitstheorie für den Handel von Kurslücken verwenden
In diesem Artikel werden wir die Wahrscheinlichkeitstheorie und die mathematischen Methoden der Statistik für das Erstellen und Testen von Handelsstrategien anwenden. Wir werden auch nach einem optimalen Handelsrisiko suchen, indem wir die Unterschiede zwischen dem Preis und dem Random Walk nutzen. Es ist bewiesen, dass, wenn sich die Preise wie ein Random Walk mit Null-Drift verhalten (ohne Richtungswechsel), ein profitabler Handel unmöglich ist.
Umkehrung: Formalisieren des Einstiegspunktes und die Entwicklung eines Algorithmus für den manuellen Handel Umkehrung: Formalisieren des Einstiegspunktes und die Entwicklung eines Algorithmus für den manuellen Handel
Dies ist der letzte Artikel innerhalb der Serie, der sich mit der Strategie des Umkehrhandels beschäftigt. Hier werden wir versuchen, das Problem zu lösen, das die Instabilität der Testergebnisse in früheren Artikeln verursacht hat. Wir werden auch unseren eigenen Algorithmus für den manuellen Handel in jedem Markt mit der Reverse-Strategie entwickeln und testen.