![Parallele Berechnungen in MetaTrader 5](https://c.mql5.com/i/articles/overlay.png)
Parallele Berechnungen in MetaTrader 5
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
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
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:
- CM - rechnergestütztes Modul. Es kann von 2 bis zur Anzahl der Prozessorkerne reichen. Und alle Codes, die parallelisiert werden müssen, müssen in das CM platziert werden. Wie wir im vorigen Abschnitt herausgefunden haben, kann das CM als ein Indikator und auch asl ein EA implementiert werden - es gibt bei jeder Form der Implementierung eine Kombination, die alle Prozessorkerne benutzt;
- MM - Hauptmodul. Führt die Hauptfunktionen des Systems aus. Wenn MM also ein Indikator ist, dann führt es das Zeichnen auf ein Chart aus; wenn MM ein EA ist, führt es Handelsfunktionen aus. Das MM verwaltet auch alle CMs.
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
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
Für die Implementierung dieses Systems müssen wir die folgenden Fragen beantworten:
- Welche der multiplen Rechnerkern-Kombinationen, die wir im vorigen Abschnitt ermittelt haben, wollen wir in unserem System verwenden?
- Da das System ja aus mehreren EAs oder Indikatoren besteht, wie können wir dann den Datenaustausch (doppelseitig) zwischen ihnen besser organisieren (also: wie sehen die Daten in der Zwischenablage physisch tatsächlich aus)?
- Und wie können wir die Koordination und Synchronisierung ihrer Handlungen organisieren?
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:
- globale Variablen des Terminals;
- Dateien;
- 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:
- MM - ist der EA, der die mit einem Muster versehene Handelsstrategie implementiert;
- parallele Berechnung wird in den CM-Indikatoren ausgeführt, die automatisch vom EA generiert werden (d.a. mit Hilfe von Kombination 7);
- der Berechnungscode in den CM-Indikatoren wird in einen unendlichen Zyklus in OnInit platziert;
- der Datenaustausch zwischen den MM-EA und CM-Indikatoren erfolgt via der globalen Variablen des Terminals.
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
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 (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)
Vergleich der Geschwindigkeiten
Nach Analyse beider Protokolle, stellen wir fest, dass die ungefähre Ausführungszeit des EA:
- im regulären Modus = 52 Sekunden;
- in einem 2-Core Modus = 27 Sekunden.
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
Der EA fand in der History das folgende ähnliche Muster:
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
Und so sehen die globale Variablen in MetaTrader 5 während der Arbeit eines EA im Parallelmodus aus
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.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/197
![Channels zeichnen - Innen- und Außenansicht](https://c.mql5.com/2/0/channels_MQL5.png)
![Design und Implementierung neuer grafischer Benutzerschnittstellen-Widgets auf Grundlage der CChartObject Klasse](https://c.mql5.com/2/0/Design_Widgets_MQL5.png)
![Erzeugung von Multi-Expert Advisors auf Grundlage von Handelsmodellen](https://c.mql5.com/2/0/Multi_Expert_Advisor_MQL5__1.png)
![Einen interaktiven, halbautomatischen Drag-and-Drop Expert Advisor auf Grundlage vorab festgelegter Risiken und dem R/R-Verhältnis (relatives Risiko) bauen](https://c.mql5.com/2/0/z5491084X.png)
![MQL5 - Sprache von Handelsstrategien, eingebaut ins Kundenterminal MetaTrader 5](https://c.mql5.com/i/registerlandings/logo-2.png)
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.