Parallele Berechnungen in MetaTrader 5

ds2 | 14 März, 2016

Einführung in den Prozessorparallelismus

Fast alle modernen PCs können mehrere Aufgaben zeitgleich ausführen - das liegt daran, dass sie alle mehrere Prozessorkerne haben. Und ihre Anzahl wächst Jahr um Jahr an - 2, 3, 4, 6 Cores... Unlängst hat Intel die Arbeit eines experimentellen 80-Core Prozessors demonstriert. Nein, kein Tippfehler, der Computer besitzt tatsächlich 80 Kerne, wird jedoch leider nicht im Handel erhältlich sein, da diese Maschine nur dafür entwickelt wurde, die potenziellen technologischen Möglichkeiten zu studieren).

Nicht alle Computer-Benutzer (und noch einmal die Neulinge unter den Programmieren) verstehen wie dieses Gerät funktioniert. Und daher wird sicherlich jemand die Frage stellen: Warum braucht man einen Rechner mit so vielen Kernen, wenn ein Computer (mit einem einzelnen Kern) zuvor doch auch schon viele Programme zeitgleich hat laufen lassen können - und das sogar problemlos? Die Wahrheit ist jedoch, das stimmt nicht so ganz. Betrachten wir uns das folgende Diagramm.

Abb. 1 Parallele Ausführung von Anwendungen

Abb. 1 Parallele Ausführung von Anwendungen

Fall A im Diagramm zeigt, was passiert, wenn ein einzelnes Programm auf einem Single-Core Prozessor ausgeführt wird. Der Prozessor widmet seine gesamte Zeit der Implementierung des Programms und das Programm arbeitet im Zeitraum T ein wenig.

Fall B - zwei Programme werden gestartet. Der Prozessor ist jedoch so konzipiert, dass zu einem gegebenen Zeitpunkt nur einer seiner Kerne physisch nur jeweils einen Befehl ausführen kann, sodass er dauernd zwischen den zwei Programmen hin- und her wechseln muss - er führt ein bisschen was vom ersten aus, dann was vom zweiten usw. Dies geschieht natürlich immens schnell, viele Male pro Sekunde, sodass es den Anschein hat, als würde der Prozessor beide Programme simultan ausführen. Doch in Wirklichkeit dauert seine Ausführung doppelt so lange, als wenn jedes Programm separat auf dem Prozessor ausgeführt werden würde.

Fall C zeigt, dass man dieses Problem wirksam lösen kann, wenn die Anzahl der Kerne in einem Prozessor der Anzahl der laufenden Programme entspricht. Jedes Programm hat dann nämlich einen separaten Kern zur Verfügung und die Geschwindigkeit der Ausführungen nimmt zu - genauso wie in Fall A.

Fall D ist die Antwort auf den Irrglauben vieler Benutzer. Sie glauben nämlich, wenn ein Programm auf einem Multi-Core Prozessor läuft, wird es auch um viele Male rascher ausgeführt. Das kann generell ja gar nicht gehen, da der Prozessor das Programm nicht unabhängig in separate Teile aufteilen und diese dann alle zeitgleich ausführen kann.

Fragt das Programm z.B. zuerst nach einem Passwort und wird dann seine Prüfung durchgeführt, könnte es die Passwortanfrage unmöglich auf einem Kern und seine Bestätigung auf einem anderen simultan ausführen. Die Bestätigung wird einfach ständig fehlschlagen, da das Passwort zum Zeitpunkt seines Inkrafttretens ja noch gar nicht eingegeben worden ist.

Der Prozessor kennt all die Designs nicht, die der Programmierer implementiert hat und er weiß auch nichts von der Logik der Arbeit des Programms und deshalb kann er das Programm eben nicht unabhängig auf seine Kerne aufteilen. Wenn wir also ein einzelnes Programm auf einem Multi-Core System laufen lassen, dann benutzen wir da nur einen Kern und es wird genauso schnell ausgeführt, als würde es auf einem Single-Core Prozesor laufen.

Fall E erklärt, was man machen muss, damit das Programm alle Kerne nutzt und schneller ausgeführt werden kann. Der Programmierer kennt die Logik des Programms, denn während seiner Entwicklung sollte er ja irgendwie die Teile des Programms markiert haben, die zeitgleich ausgeführt werden können. Während der Ausführung kommuniziert das Programm diese Information an den Prozessor, sodass dieser dann das Programm der erforderlichen Anzahl von Kernen zuweisen kann.


Parallelismus in MetaTrader

Wir haben uns gerade Gedanken gemacht, was man machen muss, um alle CPU-Kerne zu nutzen und die Ausführung von Programmen zu beschleunigen. Wir müssen irgendwie den parallelisierbaren Code des Programms separaten Threads zuweisen. Dafür gibt es in vielen Programmiersprachen spezielle Klassen oder Operatoren. Doch in der MQL5 Programmiersprache gibt es leider kein entsprechendes eingebautes Instrument. Und was jetzt?

Das Problem lässt sich auf zweierlei Wegen lösen:

1. DLL verwenden 2. Nicht programmiersprachliche Ressourcen des MetaTrader verwenden
Durch Schaffung einer DLL in einer Sprache, die ein eingebautes Tool zur Parallelisierung besitzt, erhalten wir auch im MQL5-EA Parallelisierung. Laut den Angaben der MetaTrader Entwickler ist die Architektur des Client-Terminal multi-threaded, sodass also unter gewissen Umständen, ankommende Marktdaten in separaten Threads verarbeitet werden. Wenn wir nun eine Möglichkeit finden können, den Code unserer Programms auf eine Anzahl EAs oder Indikatoren aufzuteilen, kann MetaTrader für diese Ausführung eine Anzahl von CPU-Kernen verwenden.

Die erste Methode interessiert uns in diesem Beitrag nicht. Ist ja klar: in DLL können wir implementieren, was wir wollen. Wir versuchen eine Lösung zu finden, nur mit Hilfe der Standardmöglichkeiten von MetaTrader, die nicht die Verwendung irgendwelcher anderen Programmiersprachen, außer MQL5, verlangt.

Also konzentrieren wir uns hier auf die zweite Methode. Wir müssen eine Reihe von Experimenten durchführen, um herauszufinden, wie viele Prozessorkerne genau von MetaTrader unterstützt werden. Dazu erzeugen wir einen Test-Indikator und einen Test-EA, die eine ständige Arbeit ausführen, die das CPU stark belasten.

Ich habe dazu den folgenden i-flood Indikator geschrieben:

//+------------------------------------------------------------------+
//|                                                      i-flood.mq5 |
//+------------------------------------------------------------------+
#property indicator_chart_window

input string id;
//+------------------------------------------------------------------+
void OnInit()
  {
   Print(id,": OnInit");
  }
//+------------------------------------------------------------------+
int OnCalculate(const int rt,const int pc,const int b,const double &p[])
  {
   Print(id,": OnCalculate Begin");
   
   for (int i=0; i<1e9; i++)
     for (int j=0; j<1e1; j++);
     
   Print(id,": OnCalculate End");
   return(0);   
  }
//+------------------------------------------------------------------+

Und analog den e-flood EA:

//+------------------------------------------------------------------+
//|                                                      e-flood.mq5 |
//+------------------------------------------------------------------+
input string id;
//+------------------------------------------------------------------+
void OnInit()
  {
   Print(id,": OnInit");
  }
//+------------------------------------------------------------------+
void OnTick()
  {
   Print(id,": OnTick Begin");
   
   for (int i=0; i<1e9; i++)
     for (int j=0; j<1e1; j++);
     
   Print(id,": OnTick End");
  }
//+------------------------------------------------------------------+

Durch Öffnen verschiedener Kombinationen an Chartfenstern (ein Chart, zwei Charts mit dem selben Symbol, zwei Charts mit unterschiedlichen Symbolen) und durch Platzieren einer oder zwei Kopien dieses Indikators oder EAs in sie, können wir beobachten, wie das Terminal CPU-Kerne benutzt.

Diese Indikatoren und EAs schicken auch Meldungen ans Protokoll. Hierbei ist es interessant die Abfolge ihres Auftauchens zu beobachten. Diese Protokolle zur Verfügung zu stellen, habe ich mir gespart - Sie können sie selbst generieren. In diesem Beitrag interessiert uns, herauszufinden, wie viel Kerne und in welcher Kombination an Charts vom Terminal benutzt werden.

Die Anzahl der arbeitenden Kerne kann man mit Hilfe des Windows "Task Managers" messen:

Abb. 2 CPU-Cores

Abb. 2 CPU-Cores

Die Ergebnisse aller Messungen sind in der folgenden tabelle enthalten:


Kombination
 Inhalte des Terminals CPU Nutzung
1 2 Indikatoren auf einem Chart 1 Core
2 2 Indikatoren auf unterschiedlichen Charts, das gleiche Währungspaar 1 Core
3 2 Indikatoren auf unterschiedlichen Charts, unterschiedliche Währungspaare 2 Cores
4 2 EAs auf demselben Chart - das ist nicht möglich -
5 2 EAs auf unterschiedlichen Charts, das gleiche Währungspaar 2 Cores
6 2 EAs auf unterschiedlichen Charts, unterschiedliche Währungspaare 2 Cores
7 2 Indikatoren auf unterschiedlichen Paaren, vom EA erzeugt 2 Cores

Die 7. Kombination wird in vielen Handelsstrategien häufig zur Erzeugung eines Indikators genutzt.

Das einzige Spezialfeature hier ist, dass ich zwei Indikatoren auf zwei unterschiedlichen Währungspaaren erzeugt habe, da ja aus Kombination 1 und 2 schon klar wird, dass es unsinnig ist, die Indikatoren auf das gleiche Paar zu platzieren. Für diese Kombination habe ich den EA e-flood-Starter benutzt, der zwei Kopien von i-flood erzeugte:

//+------------------------------------------------------------------+
//|                                              e-flood-starter.mq5 |
//+------------------------------------------------------------------+
void OnInit() 
  {
   string s="EURUSD";
   for(int i=1; i<=2; i++) 
     {
      Print("Indicator is created, handle=",
            iCustom(s,_Period,"i-flood",IntegerToString(i)));
      s="GBPUSD";
     }
  }
//+------------------------------------------------------------------+

Somit wurden alle Berechnungen der Cores ausgeführt und wir wissen jetzt für welche Kombinationen MetaTrader mehrere Prozessorkerne verwendet. Dieses Wissen versuchen wir nun für die Implementierung der Konzepte des parallelen Rechnens anzuwenden.

Wir entwerfen ein paralleles System

Im Hinblick auf das Handelsterminal für das parallele System, meinen wir damit eine Gruppe Indikatoren oder EAs (oder eine Mischung an beiden), die gemeinsam eine normale Aufgabe ausführen, z.B. einen Handel ausführen oder etwas auf ein Chart zeichnen. D.h.: diese Gruppe arbeitet als ein großer Indikator oder ein großer EA, verteilt jedoch zugleich die Rechnerlast auf alle verfügbaren Prozessorkerne

So ein System besteht aus zwei Arten Softwarekomponenten:

Das Arbeitsschema des Systems für ein MM EA und einem 2-Core Prozessor sieht beispielsweise so aus:

Abb. 3 Systemschema mit 2 CPU Cores.

Abb. 3 Systemschema mit 2 CPU Cores

Hier sollte man jedoch verstehen, dass das von uns entwickelte System kein herkömmliches Programm ist, bei dem man in jedem Vorgang zu jeder Zeit das Erforderliche einfach aufrufen kann. MM und CM sind EAs oder Indikatoren, d.h. sie sind unabhängige und Stand-Alone Programme Zwischen ihnen gibt es keine Verbindung, sie arbeiten komplett eigenständig und unabhängig und können nicht direkt miteinander kommunizieren.

Die Ausführung jedes dieser Programme beginnt nur beim Auftauchen eines Ereignisses im Terminal (z.B. das Auftauchen von Quoten oder einem Timer-Tick). Und zwischen den Ereignissen müssen alle Daten, die diese Programme sich gegenseitig übermitteln wollen, irgendwo außerhalb der Programme, an einem öffentlich zugänglichen Ort, abgelegt werden (nennen wir ihn den "Datenaustausch-Puffer"). Daher wird das obige Schema folgendermaßen in den Terminal implementiert:

Abb. 4 Implementierungsdetails

Abb. 4 Implementierungsdetails

Für die Implementierung dieses Systems müssen wir die folgenden Fragen beantworten:

Auf jede dieser Fragen gibt es mehr als eine Antwort - und Sie erfahren sie alle im Folgenden. In der Praxis sollten die spezifischen Optionen je nach der jeweiligen Situation ausgewählt werden. Das ist das Thema des nächsten Abschnitts. Doch bis dahin sehen wir uns alle möglichen Antworten an.

Kombination

Die Kombination 7 ist für eine regelmäßige, praktische Verwendung wohl am bequemsten (alle anderen Kombinationen sind im vorigen Abschnitt aufgezählt), da man bei ihr keine zusätzlichen Fenster im Terminal öffnen und in sie EAs oder Indikatoren platzieren muss. Das gesamte System befindet sich in einem einzigen Fenster und alle Indikatoren (CM-1 und CM-2) werden vom EA (MM) automatisch erzeugt. Das Fehlen der extra Fensters und der manuellen Handlungen beseitigen somit beim Händler entsprechenden Verwirrungen und eben auch die, mit derlei Unklarheiten zusammenhängenden, Fehler.

Bei einigen Handelsstrategien können andere Kombinationen hilfreich sein. So können wir z.B auf Grundlage jeder dieser Kombinationen komplette Software-Systeme erzeugen, die nach dem "Client-Server" Prinzip funktionieren. Wo die gleichen CMs für unterschiedliche MMs gemeinsam sind. Solche gemeinsame CMS können nicht nur die sekundäre Rolle eines "Computer" ausführen, sondern auch ein "Server" sein, der irgendeine Art für alle Strategien vereinheitlichter Information aufbewahrt, oder sie können sogar die Koordination ihrer gemeinsamen Arbeit übernehmen. Ein CM-Server könnten beispielsweise die Verteilung der Möglichkeiten in einem Strategie-Portfolio und bei Währungspaaren zentral kontrollieren und dabei das gewünschte insgesamte Risikolevel beibehalten.

Datenaustausch

Informationen zwischen dem MM und dem CM können mittels jedem der drei folgenden Wege übertragen werden:

  1. globale Variablen des Terminals;
  2. Dateien;
  3. Indikatorpuffer.

Die erste Methode ist optimal, wenn nur eine kleine Anzahl numerischer Variablen übertragen werden muss. Müssen Textdaten übertragen werden, dann müssen die bis zu einem gewissen Grad in Ziffern codiert werden, denn globale Variablen sind nur vom Typ 'double'.

Die zweite Methode bietet hier eine Alternative: denn alles kann in eine Datei(en) geschrieben werden. Außerdem ist dies eine bequeme, und vermutlich schnellere Methode, als Variante 1, in Situationen, wo sie große Datenmengen übertragen müssen.

Die dritte Methode eignet sich gut, wenn das MM und CM Indikatoren sind. Es können nur Daten des Typs 'double' übertragen werden, doch ist dies zum Übertragen umfangreicher Zahlen-Arrays wesentlich bequemer. Doch einen Nachteil gibt es: Während der Herausbildung eines neuen Balkens verschiebt sich die Nummerierung der Elemente in den Puffern. Da sich ja MM und CM auf unterschiedlichen Währungspaaren befinden, tauchen die neuen Balken nicht zeitgleich auf. Diese Verschiebungen müssen wir berücksichtigen.

Synchronisierung

Sobald das Terminal eine Quote für das MM erhält und diese zu verarbeiten beginnt, kann es nicht sofort die Kontrolle an das CM übertragen. Es kann nur eine Aufgabe herausbilden (wie im Diagramm oben dargestellt), wie z.B. Platzierung der Quote in die globalen Variablen, in eine Datei oder einen Indikator-Puffer, und darauf warten, bis das CM ausgeführt wird. Da sich alle CMs auf unterschiedlichen Währungspaaren befinden, muss man u.U. einige Zeit warten. Das liegt daran, dass evtl. ein Währungspaar die Quote bereits erhält, sie bei einem anderen aber noch nicht angekommen hat und sie nur in wenigen Sekunden oder erst nach einigen Minuten dort auftaucht (das passiert z.B. gerne nachts bei nicht-liquiden Währungspaaren).

Damit also das CM die Kontrolle bekommt, sollten wir nicht mit den OnTick und OnCalculate Ereignissen arbeiten, die ja von Quoten abhängen, sondern stattdessen das OnTimer Ereignis verwenden (eine MQL5 Innovation), das mit einer festgelegten Häufigkeit ausgeführt wird (z.B. jede Sekunde). In diesem Fall sind die Verzögerung im System erheblich kürzer.

Anstelle von OnTimer können wir auch die Zyklustechnik verwenden, d.h. einen unendlichen Zyklus für das CM in OnInit oder OnCalculate einsetzen. Jede seiner Wiederholungen ist ein Gegenstück eines Timer-Tick.

Warnung. Ich habe einige Experimente ausgeführt und festgestellt, dass das OnTimer Ereignis bei Kombination 7 aus irgendeinem Grund nicht in den Indikatoren funktioniert, obwohl die Timer alle erfolgreich erzeugt wurden.

Außerdem müssen Sie mit den unendlichen Schleifen in OnInit und OnCalculate aufpassen: sollte sich bereits nur ein einziger CM-Indikator auf dem selben Währungspaar wie der MM-EA befinden, bewegt sich der Preis auf dem Chart nicht mehr und der EA beendet seine Arbeit (er generiert also keine OnTick Ereignisse mehr). Die Entwickler des Terminals haben die Gründe für so ein Verhalten erklärt.

Sie erklärten das so: Scripts und EAs arbeiten in ihren eigenen, separaten Threads, wohingegen alle Indikatoren auf einem einzigen Symbol im selben Thread arbeiten. Und alle anderen Handlungen auf diesem Symbol werden im selben Stream wie die Indikatoren nacheinander ausgeführt: die Verarbeitung von Kursschwankungen, die Synchronisierung der History und die Berechnung von Indikatoren. Wenn als der Indikator eine unendliche Handlung ausführt, werden alle anderen Ereignisse für sein Symbol niemals ausgeführt.

Programm Ausführung Hinweis
Script In seinem eigenen Thread gibt es so viele Threads der Ausführung wie es Scripts gibt Ein zyklisches Script kann die Arbeit anderer Programme nicht stören
Expert Advisor In seinem eigenen Thread gibt es so viele Threads der Ausführung wie es EAs gibt Ein zyklisches Script kann die Arbeit anderer Programme nicht stören
Indikator Ein Thread der Ausführung für alle Indikatoren auf einem Symbol. Es gibt so viele Symbole mit Indikatoren wie es Threads der Ausführung für sie gibt Ein unendlicher Zyklus in einem Indikator hält die Arbeit aller anderen Indikatoren auf diesem Symbol an

Einen Test-Expert Advisor erzeugen

Suchen wir uns eine Handelsstrategie aus, bei der eine 'Parallelisierung' Sinn machen würde und auch einen dafür passenden Algorithmus.

Das kann durchaus einen einfache Strategie sein: das Kompilieren der Sequenz von N der letzten Balken und Finden der Sequenz in der History, die dieser am stärksten ähnelt. Da wir wissen, wohin sich der Kurs in der History verschoben hat, eröffnen wir den entsprechenden Abschluss.

Ist die Länge der Sequenz relativ kurz, funktioniert diese Strategie in MetaTrader 5 sehr schnell- innerhalb von Sekunden. Doch bei einer großen Länge - z.B. alle Balken des Zeitrahmens M! für die letzten 24 Stunden (also 1440 Balken) - und wenn wir zudem in der History mit unserer Suche bis zu einem Jahr zurückgehen (ca. 375.000 Balken), dann braucht dieser Vorgang eine Menge Zeit. Und dennoch kann man diese Sucher leicht parallelisieren. Dazu genügt es die History gleichmäßig auf die Anzahl der vorhandenen Prozessorkerne aufzuteilen und jedem Kern einen spezifischen Standort zum Durchsuchen zuweisen.

Die Parameter des Paralellsystems sehen dann so aus:

Aus Gründen einer bequemen Entwicklung und vor allem eines geschmeidigen späteren Einsatzes erzeugen wir den EA so, dass er, je nach den Einstellungen, sowohl parallel (mit Berechnungen in den Indikatoren) als auch auf herkömmliche Art (also ohne Verwendung von Indikatoren) arbeiten könnte. Das ist der Code des so erhaltenen e-MultiThread Expert Advisors:

//+------------------------------------------------------------------+
//|                                                e-MultiThread.mq5 |
//+------------------------------------------------------------------+
input int Threads=1; // How many cores should be used
input int MagicNumber=0;

// Strategy parameters
input int PatternLen  = 1440;   // The length of the sequence to analyze (pattern)
input int PrognozeLen = 60;     // Forecast length (bars)
input int HistoryLen  = 375000; // History length to search

input double Lots=0.1;
//+------------------------------------------------------------------+
class IndData
  {
public:
   int               ts,te;
   datetime          start_time;
   double            prognoze,rating;
  };

IndData Calc[];
double CurPattern[];
double Prognoze;
int  HistPatternBarStart;
int  ExistsPrognozeLen;
uint TicksStart,TicksEnd;
//+------------------------------------------------------------------+
#include <ThreadCalc.mqh>
#include <Trade\Trade.mqh>
//+------------------------------------------------------------------+
int OnInit()
  {

   double rates[];

//--- Make sure there is enough history
   int HistNeed=HistoryLen+Threads+PatternLen+PatternLen+PrognozeLen-1;
   if(TerminalInfoInteger(TERMINAL_MAXBARS)<HistNeed)
     {    
      Print("Change the terminal setting \"Max. bars in chart\" to the value, not lesser than ",
            HistNeed," and restart the terminal");
      return(1);      
     }
   while(Bars(_Symbol,_Period)<HistNeed)
     {      
      Print("Insufficient history length (",Bars(_Symbol,_Period),") in the terminal, upload...");
      CopyClose(_Symbol,_Period,0,HistNeed,rates);
     }
   Print("History length in the terminal: ",Bars(_Symbol,_Period));

//--- For a multi-core mode create computational indicators
   if(Threads>1)
     {
      GlobalVarPrefix="MultiThread_"+IntegerToString(MagicNumber)+"_";
      GlobalVariablesDeleteAll(GlobalVarPrefix);

      ArrayResize(Calc,Threads);

      // Length of history for each core
      int HistPartLen=MathCeil(HistoryLen/Threads);
      // Including the boundary sequences
      int HistPartLenPlus=HistPartLen+PatternLen+PrognozeLen-1;

      string s;
      int snum=0;
      // Create all computational indicators
      for(int t=0; t<Threads; t++)
        {      
         // For each indicator - its own currency pair,
         // it should not be the same as for the EA
         do
            s=SymbolName(snum++,false);
         while(s==_Symbol);

         int handle=iCustom(s,_Period,"i-Thread",
                            GlobalVarPrefix,t,_Symbol,PatternLen,
                            PatternLen+t*HistPartLen,HistPartLenPlus);

         if(handle==INVALID_HANDLE) return(1);
         Print("Indicator created, pair ",s,", handle ",handle);
        }
     }

   return(0);
  }
//+------------------------------------------------------------------+
void OnTick()
  {
   TicksStart=GetTickCount();

   // Fill in the sequence with the last bars
   while(CopyClose(_Symbol,_Period,0,PatternLen,CurPattern)<PatternLen) Sleep(1000);

   // If there is an open position, measure its "age"
   // and modify the forecast range for the remaining 
   // planned life time of the deal
   CalcPrognozeLen();

   // Find the most similar sequence in the history
   // and the forecast of the movement of its price on its basis
   FindHistoryPrognoze();

   // Perform the necessary trade actions
   Trade();

   TicksEnd=GetTickCount();
   // Debugging information in
   PrintReport();
  }
//+------------------------------------------------------------------+
void FindHistoryPrognoze()
  {
   Prognoze=0;
   double MaxRating;

   if(Threads>1)
     {
      //--------------------------------------
      // USE COMPUTATIONAL INDICATORS
      //--------------------------------------
      // Look through all of the computational indicators 
      for(int t=0; t<Threads; t++)
        {
         // Send the parameters of the computational task
         SetParam(t,"PrognozeLen",ExistsPrognozeLen);
         // "Begin computations" signal 
         SetParam(t,"Query");
        }

      for(int t=0; t<Threads; t++)
        {
         // Wait for results
         while(!ParamExists(t,"Answer"))
            Sleep(100);
         DelParam(t,"Answer");

         // Obtain results
         double progn        = GetParam(t, "Prognoze");
         double rating       = GetParam(t, "Rating");
         datetime time[];
         int start=GetParam(t,"PatternStart");
         CopyTime(_Symbol,_Period,start,1,time);
         Calc [t].prognoze   = progn;
         Calc [t].rating     = rating;
         Calc [t].start_time = time[0];
         Calc [t].ts         = GetParam(t, "TS");
         Calc [t].te         = GetParam(t, "TE");

         // Select the best result
         if((t==0) || (rating>MaxRating))
           {
            MaxRating = rating;
            Prognoze  = progn;
           }
        }
     }
   else
     {
      //----------------------------
      // INDICATORS ARE NOT USED
      //----------------------------
      // Calculate everything in the EA, into one stream
      FindPrognoze(_Symbol,CurPattern,0,HistoryLen,ExistsPrognozeLen,
                   Prognoze,MaxRating,HistPatternBarStart);
     }
  }
//+------------------------------------------------------------------+
void CalcPrognozeLen()
  {
   ExistsPrognozeLen=PrognozeLen;

   // If there is an opened position, determine 
   // how many bars have passed since its opening
   if(PositionSelect(_Symbol))
     {
      datetime postime=PositionGetInteger(POSITION_TIME);
      datetime curtime,time[];
      CopyTime(_Symbol,_Period,0,1,time);
      curtime=time[0];
      CopyTime(_Symbol,_Period,curtime,postime,time);
      int poslen=ArraySize(time);
      if(poslen<PrognozeLen)
         ExistsPrognozeLen=PrognozeLen-poslen;
      else
         ExistsPrognozeLen=0;
     }
  }
//+------------------------------------------------------------------+
void Trade()
  {

   // Close the open position, if it is against the forecast
   if(PositionSelect(_Symbol))
     {
      long type=PositionGetInteger(POSITION_TYPE);
      bool close=false;
      if((type == POSITION_TYPE_BUY)  && (Prognoze <= 0)) close = true;
      if((type == POSITION_TYPE_SELL) && (Prognoze >= 0)) close = true;
      if(close)
        {
         CTrade trade;
         trade.PositionClose(_Symbol);
        }
     }

   // If there are no position, open one according to the forecast
   if((Prognoze!=0) && (!PositionSelect(_Symbol)))
     {
      CTrade trade;
      if(Prognoze > 0) trade.Buy (Lots);
      if(Prognoze < 0) trade.Sell(Lots);
     }
  }
//+------------------------------------------------------------------+
void PrintReport()
  {
   Print("------------");
   Print("EA: started ",TicksStart,
         ", finished ",TicksEnd,
         ", duration (ms) ",TicksEnd-TicksStart);
   Print("EA: Forecast on ",ExistsPrognozeLen," bars");

   if(Threads>1)
     {
      for(int t=0; t<Threads; t++)
        {
         Print("Indicator ",t+1,
               ": Forecast ", Calc[t].prognoze,
               ", Rating ", Calc[t].rating,
               ", sequence from ",TimeToString(Calc[t].start_time)," in the past");
         Print("Indicator ",t+1,
               ": started ",  Calc[t].ts,
               ", finished ",   Calc[t].te,
               ", duration (ms) ",Calc[t].te-Calc[t].ts);
        }
     }
   else
     {
      Print("Indicators were not used");
      datetime time[];
      CopyTime(_Symbol,_Period,HistPatternBarStart,1,time);
      Print("EA: sequence from ",TimeToString(time[0])," in the past");
     }

   Print("EA: Forecast ",Prognoze);
   Print("------------");
  }
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   // Send the "finish" command to the indicators
   if(Threads>1)
      for(int t=0; t<Threads; t++)
         SetParam(t,"End");
  }
//+------------------------------------------------------------------+

Und das ist der Code des rechnerischen Indikators i-Thread, der vom Expert Advisor verwendet wird:

//+------------------------------------------------------------------+
//|                                                     i-Thread.mq5 |
//+------------------------------------------------------------------+
#property indicator_chart_window
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1

//--- input parameters
input string VarPrefix;  // Prefix for global variables (analog to MagicNumber)
input int    ThreadNum;  // Core number (so that indicators on different cores could
                        // differentiate their tasks from the tasks of the "neighboring" cores)
input string DataSymbol; // On what pair is the MM-EA working
input int    PatternLen; // Length of the sequence for analysis
input int    BarStart;   // From which bar in the history the search for a similar sequence began
input int    BarCount;   // How many bars of the history to perform a search on

//--- indicator buffers
double Buffer[];
//---
double CurPattern[];

//+------------------------------------------------------------------+
#include <ThreadCalc.mqh>
//+------------------------------------------------------------------+
void OnInit()
  {
   SetIndexBuffer(0,Buffer,INDICATOR_DATA);

   GlobalVarPrefix=VarPrefix;

   // Infinite loop - so that the indicator always "listening", 
   // for new commands from the EA
   while(true)
     {
      // Finish the work of the indicator, if there is a command to finish
      if(ParamExists(ThreadNum,"End"))
         break;

      // Wait for the signal to begin calculations
      if(!ParamExists(ThreadNum,"Query"))
        {
         Sleep(100);
         continue;
        }
      DelParam(ThreadNum,"Query");

      uint TicksStart=GetTickCount();

      // Obtain the parameters of the task
      int PrognozeLen=GetParam(ThreadNum,"PrognozeLen");

      // Fill the sequence from the last bars
      while(CopyClose(DataSymbol,_Period,0,PatternLen,CurPattern)
            <PatternLen) Sleep(1000);

      // Perform calculations
      int HistPatternBarStart;
      double Prognoze,Rating;
      FindPrognoze(DataSymbol,CurPattern,BarStart,BarCount,PrognozeLen,
                   Prognoze,Rating,HistPatternBarStart);

      // Send the results of calculations
      SetParam(ThreadNum,"Prognoze",Prognoze);
      SetParam(ThreadNum,"Rating",Rating);
      SetParam(ThreadNum,"PatternStart",HistPatternBarStart);
      SetParam(ThreadNum,"TS",TicksStart);
      SetParam(ThreadNum,"TE",GetTickCount());
      // Signal "everything is ready"
      SetParam(ThreadNum,"Answer");
     }
  }
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {
   // The handler of this event is required
   return(0);
  }
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   SetParam(ThreadNum,"End");
  }
//+------------------------------------------------------------------+

Der Expert Advisor und der Indikator verwenden beide eine gemeinsame ThreadCalc.mqh Library.

Und das ist der Code:

//+------------------------------------------------------------------+
//|                                                   ThreadCalc.mqh |
//+------------------------------------------------------------------+
string GlobalVarPrefix;
//+------------------------------------------------------------------+
// It finds the price sequence, most similar to the assigned one.
// in the specified range of the history
// Returns the estimation of similarity and the direction 
// of the further changes of prices in history.
//+------------------------------------------------------------------+
void FindPrognoze(
                  string DataSymbol,    // symbol
                  double  &CurPattern[],// current pattern
                  int BarStart,         // start bar
                  int BarCount,         // bars to search
                  int PrognozeLen,      // forecast length

                  // RESULT
                  double  &Prognoze,        // forecast (-,0,+)
                  double  &Rating,          // rating
                  int  &HistPatternBarStart // starting bar of the found sequence
                  ) 
  {

   int PatternLen=ArraySize(CurPattern);

   Prognoze=0;
   if(PrognozeLen<=0) return;

   double rates[];
   while(CopyClose(DataSymbol,_Period,BarStart,BarCount,rates)
         <BarCount) Sleep(1000);

   double rmin=-1;
   // Shifting by one bar, go through all of the price sequences in the history
   for(int bar=BarCount-PatternLen-PrognozeLen; bar>=0; bar--) 
     {
      // Update to eliminate the differences in the levels of price in the sequences
      double dr=CurPattern[0]-rates[bar];

      // Calculate the level of differences between the sequences - as a sum 
      // of squares of price deviations from the sample values
      double r=0;
      for(int i=0; i<PatternLen; i++)
         r+=MathPow(MathAbs(rates[bar+i]+dr-CurPattern[i]),2);

      // Find the sequence with the least difference level
      if((r<rmin) || (rmin<0)) 
        {
         rmin=r;
         HistPatternBarStart   = bar;
         int HistPatternBarEnd = bar + PatternLen-1;
         Prognoze=rates[HistPatternBarEnd+PrognozeLen]-rates[HistPatternBarEnd];
        }
     }
   // Convert the bar number into an indicator system of coordinates
   HistPatternBarStart=BarStart+BarCount-HistPatternBarStart-PatternLen;

   // Convert the difference into the rating of similarity
   Rating=-rmin;
  }
//====================================================================
// A set of functions for easing the work with global variables.
// As a parameter contain the number of computational threads 
// and the names of the variables, automatically converted into unique
// global names.
//====================================================================
//+------------------------------------------------------------------+
string GlobalParamName(int ThreadNum,string ParamName) 
  {
   return GlobalVarPrefix+IntegerToString(ThreadNum)+"_"+ParamName;
  }
//+------------------------------------------------------------------+
bool ParamExists(int ThreadNum,string ParamName) 
  {
   return GlobalVariableCheck(GlobalParamName(ThreadNum,ParamName));
  }
//+------------------------------------------------------------------+
void SetParam(int ThreadNum,string ParamName,double ParamValue=0) 
  {
   string VarName=GlobalParamName(ThreadNum,ParamName);
   GlobalVariableTemp(VarName);
   GlobalVariableSet(VarName,ParamValue);
  }
//+------------------------------------------------------------------+
double GetParam(int ThreadNum,string ParamName) 
  {
   return GlobalVariableGet(GlobalParamName(ThreadNum,ParamName));
  }
//+------------------------------------------------------------------+
double DelParam(int ThreadNum,string ParamName) 
  {
   return GlobalVariableDel(GlobalParamName(ThreadNum,ParamName));
  }
//+------------------------------------------------------------------+

Unser Handelssystem, das für seine Arbeit mehr als einen Prozessorkern benutzen kann, ist fertig!

Wenn Sie es verwenden, sollten Sie daran denken, dass wir in diesem Beispiel hier CM-Indikatoren mit unendlichen Schleifen verwendet haben.

Wenn sie also vorhaben, zusammen mit diesem System im Terminal noch andere Programme laufen zu lassen, dann sollten Sie sichergehen, dass Sie sie auf den Währungspaaren einsetzen, die nicht von den CM-Indikatoren besetzt sind. Eine gute Möglichkeit, derartige Verwirrung zu vermeiden, ist das System dahingehend zu modifizieren, das Sie in den Eingabe-Parameters des MM-EA direkt die Währungspaare für die CM-Indikatoren festlegen können.

Messung der Arbeitsgeschwindigkeit des EAs

Regulärer Modus

Öffnen Sie das EURUSD M1 Chart und starten den Expert Advisor, den wir im vorigen Abschnitt erzeugt haben. Legen Sie unter 'Einstellungen' die Länge der Muster als 24 Stunden fest (1440 Minuten-Balken), und die Intensität der Suche in der History auf 1 Jahr (375.000 Balken).

Abb. 5 Expert Advisor Eingabeparameter

Abb. 5 Expert Advisor Eingabeparameter

Der Parameter "Threads" wird = 1 gesetzt. Das heißt, dass alle Berechnungen des EA in einem Thread (auf einem einzigen Prozessorkern) erfolgen. Ich verwenden inzwischen keine Berechnungsindikatoren, sondern berechne alles selbst, und zwar grundsätzlich nach dem Prinzip der Arbeit eines regulären EAs.

Protokoll seiner Ausführung:

Abb. 6 Expert Advisor Protokoll

Abb. 6 Expert Advisor Protokoll (1 Thread)

Parallelmodus

So jetzt löschen wir diesen EA und die von ihm eröffnete Position, und fügen ihn erneut hinzu, diesmal jedoch mit dem Parameter "Threads" = 2.

Der EA muss nun 2 Berechnungsindikatoren erzeugen und für seine Arbeit verwenden, und besetzt somit zwei Prozessorkerne. Protokoll seiner Ausführung:

Abb. 7 Expert Advisor Protokoll (2 Threads)

Abb. 7 Expert Advisor Protokoll (2 Threads)

Vergleich der Geschwindigkeiten

Nach Analyse beider Protokolle, stellen wir fest, dass die ungefähre Ausführungszeit des EA:

Indem wir also auf einem 2-Core CPU Parallelisierung durchgeführt haben, konnten wir die Geschwindigkeit eines EA um das 1,9-fache steigern. Man kann daher davon ausgehen, dass, wenn man einen Prozessor mit einer großen Anzahl an Kernen verwendet, diese Geschwindigkeit , im Verhältnis der vorhandenen Kerne sogar noch weiter ansteigt.

Kontrolle der Exaktheit der Arbeit

Zusätzlich zur für die Ausführung benötigten Zeit liefern uns die Protokolle noch weitere Informationen, mit denen wir verifizieren können, dass alle Messungen korrekt ausgeführt wurden. Die Zeilen im EA: "Arbeit gestartet ... Arbeit beendet ..." "und "Indikator ...: Arbeit gestartet ... Arbeit beendet ..." geben an, dass die Indikatoren ihre Berechnungen nicht eine Sekunde vorher begonnen haben, als sie vom EA dazu den entsprechenden Befehl erhalten haben.

Wir wollen auch überprüfen, dass es während des Starts des EA im Parallelmodus zu keinerlei Verletzungen der Handelsstrategie gekommen ist. Laut Protokolle ist es klar, dass der Start des EA im Parallelmodus fast unmittelbar nach seinem Start im regulären Modus erfolgt ist. Was bedeutet: die Marktsituation war in beiden Fällen sehr ähnlich. Zudem zeigen die Protokolle, dass die Daten, die in der History der Muster gefunden wurden, in beiden Fällen ebenfalls sehr ähnlich waren. Passt also alles: der Algorithmus der Strategie arbeitet in beiden Fällen gleichermaßen gut.

Und das sind die in den Protokollsituationen beschriebenen Muster. Das war die aktuelle Marktsituation (Länge - 1440-Minutenbalken) als der EA im regulären Modus lief:

Abb. 8 Aktuelle Marktsituation

Abb. 8 Aktuelle Marktsituation

Der EA fand in der History das folgende ähnliche Muster:

Abb. 9 Ähnliche Marktsituation

Abb. 9 Ähnliche Marktsituation

Als der EY im Parallelmodus lief, wurde dasselbe Muster durch "Indikator 1" gefunden. "Indikator 2", so können wir es dem Protokoll entnehmen, hat im anderen Halbjahr der History nach Mustern gesucht und daher ein anderes, ähnliches Muster gefunden:

Abb. 10 Ähnliche Marktsituation

Abb. 10 Ähnliche Marktsituation

Und so sehen die globale Variablen in MetaTrader 5 während der Arbeit eines EA im Parallelmodus aus

Abb. 11 Globale Variablen

Abb. 11 Globale Variablen

Der Datenaustausch zwischen dem EA und den Indikatoren durch globale Variablen wurde erfolgreich implementiert.

Fazit

In diesem Versuch haben wir festgestellt, dass es möglich ist, einfallsreiche Algorithmen mit Hilfe der Standardmöglichkeiten von MetaTrader 5 zu parallelisieren. Und die Lösung für dieses Problem, die wir in diesem Beitrag gefunden und beschrieben haben, ist für eine Verwendung bei Handelsstrategien der echten Welt extrem bequem.

In einem Multi-Core System arbeitet dieses Programm in der Tat proportional erheblich schneller. Die Anzahl der Prozessorkerne in Rechnern nimmt jedes Jahr zu, sodass es gut ist, dass Händel, die mit MetaTrader arbeiten, nun eine Möglichkeit an der Hand haben, diese Hardware-Ressourcen effektiv nutzen zu können. Wir können als sicher weitere Handelsstrategien voller Ressourcen erzeugen, die den Markt in Echtzeit analysieren können.