![Individuell Strategien testen basierend auf schnellen mathematischen Berechnungen](https://c.mql5.com/i/articles/overlay.png)
Individuell Strategien testen basierend auf schnellen mathematischen Berechnungen
Inhalt
- Einführung
- Allgemeine Informationen über die "Mathematische Berechnung"
- Speichern der historischen Daten eines Symbols für den Tester als Basis für "Mathematischen Berechnung"
- Laden von MqlRates aus einem komprimierten Byte-Array
- Beispielstrategie auf der Basis eines gleitenden Mittelwerts
- Klasse virtueller Positionen
- Klasse für den Handel
- Die erste Prüfung des Expert Advisors. Optimieren
- Speichern der benutzerdefinierten Optimierungsergebnissen mit Hilfe des Frame-Mechanismus
- Erhalten einer Byte-Darstellung der Positionshistorie. Speichern von Daten in Frames
- Strategy Analyzer anlegen
- Schlussfolgerung
Einführung
Der in MetaTrader 5 enthaltene Strategy Tester verfügt über leistungsstarke Funktionen zur Lösung einer Vielzahl von Aufgaben. Mit ihm können sowohl komplexe Strategien für den Handel mit Gruppen von Symbolen als auch Einzelstrategien mit einfachen Ein- und Ausstiegsregeln getestet werden. Eine derart umfangreiche Funktionalität erweist sich jedoch nicht immer als nützlich. Häufig müssen wir nur schnell eine einfache Handelsidee überprüfen oder ungefähre Berechnungen anstellen, deren Genauigkeit durch ihre Geschwindigkeit kompensiert wird. Der Standard-Tester in MetaTrader 5 hat eine interessante, aber selten genutzte Funktion: Die Test mittels der "Mathematischen Berechnung". Dies ist ein eingeschränkter Modus für den Betrieb des Strategietesters, der dennoch alle Vorteile einer vollwertigen Optimierung bietet: Cloud Computing ist verfügbar, ein genetischer Optimierer kann verwendet werden und es ist möglich, benutzerdefinierte Datentypen zu sichern.
Ein individueller Strategietest könnte von Vorteil sein, nicht nur dann, wenn man sich die absolute Geschwindigkeit wünscht. Das Testen mit der "Mathematischen Berechnung" öffnet auch Forschern den Weg. Der standardmäßige Strategietester ermöglicht es, Handelsoperationen so realitätsnah wie möglich zu simulieren. Diese Anforderung ist für manche Untersuchung nicht sinnvoll. Beispielsweise ist es manchmal notwendig, eine Schätzung der Nettogewinne eines Handelssystems zu erhalten, ohne dabei Slippage, Spread und Kommission zu berücksichtigen. Der in diesem Artikel entwickelte Tester für "Mathematische Berechnung" bietet eine solche Fähigkeit.
Natürlich lässt sich auch damit kein Kreis quadrieren. Dieser Artikel ist keine Ausnahme. Das Schreiben eines benutzerdefinierten Strategie-Testers erfordert ernsthafte und zeitaufwändige Arbeit. Das Ziel ist bescheiden: Es gilt zu zeigen, dass es mit den richtigen Bibliotheken nicht so schwierig ist, einen benutzerdefinierten Tester zu erstellen, wie es auf den ersten Blick scheint.
Wenn dieses Thema meine Leser interessiert, wird dieser Artikel eine Fortsetzung sehen, die die vorgeschlagenen Ideen weiterentwickelt.
Allgemeine Informationen über die "Mathematische Berechnung"
Die "Mathematische Berechnung" wird im Fenster des Strategietesters gestartet. Wählen Sie dazu in der Dropdown-Liste den gleichnamigen Menüpunkt aus:
Abb. 1. Auswahl der "Mathematischen Berechnung" im Strategietester
Dieser Modus ruft nur eine begrenzte Anzahl von Funktionen auf, die Handelsumgebung (Symbole, Kontoinformationen, Eigenschaften des Handelsservers) ist nicht verfügbar. OnTester() wird zur Hauptaufruf-Funktion, die von Benutzern verwendet werden kann, um ein spezielles, benutzerdefiniertes Optimierungskriterium festzulegen. Es wird zusammen mit anderen standardmäßigen Optimierungskriterien verwendet und kann im standardmäßigen Strategiebericht angezeigt werden. Sie ist im folgenden Screenshot rot umrandet:
Abb. 2. Benutzerdefiniertes Optimierungskriterium, das in der Funktion OnTester berechnet wird.
Die von der Funktion OnTester zurückgegebenen Werte sind auswählbar und optimierbar. Lassen Sie uns dies an einem einfachen Experten demonstrieren:
//+------------------------------------------------------------------+ //| OnTesterCheck.mq5 | //| Copyright 2017, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2017, MetaQuotes Software Corp." #property link "http://www.mql5.com" #property version "1.00" input double x = 0.01; //+------------------------------------------------------------------+ //| Tester function | //+------------------------------------------------------------------+ double OnTester() { double ret = MathSin(x); return(ret); } //+------------------------------------------------------------------+
Sein Code enthält nichts anderes als den Eingabeparameter x und die Funktion OnTester, die aus dem übergebenen Argument den Sinuswert berechnet. In diesem Fall ist es x. Versuchen Sie nun, diese Funktion zu optimieren. Wählen Sie dazu im Strategietester den Optimierungsmodus "Langsame vollständige Parametersuche" und den bisherigen Simulationsmodus "Mathematische Berechnung".
Stellen Sie den Variationsbereich von x in den Optimierungsparameter ein: Start — 0,01, Schritt — 0,01, Stop — 10. Sobald alles fertig ist, starten Sie den Strategietester. Es wird seine Arbeit fast sofort beenden. Öffnen Sie anschließend den Optimierungsgraphen und wählen Sie im Kontextmenü "1D-Grafik". Dies zeigt eine Sinusfunktion in der grafischen Interpretation:
Abb. 3. Grafische Darstellung der Sinusfunktion
Eine Besonderheit dieses Modus ist der minimale Ressourcenverbrauch. Die Lese-Schreib-Operationen auf der Festplatte werden minimiert, Tester-Agenten laden keine Kurse der angeforderten Symbole herunter, keine zusätzlichen Berechnungen, alle Berechnungen werden in der OnTester-Funktion fokussiert.
Aufgrund der möglichen hohen Geschwindigkeit von OnTester ist es möglich, ein autarkes Berechnungsmodul zu erstellen, das einfache Simulationen durchführen kann. Hier sind die Elemente dieses Moduls:
- Geschichte des Symbols für die Prüfung
- System der virtuellen Positionen
- Handelssystem zur Verwaltung virtueller Positionen
- System der Ergebnisanalyse
Die Eigenständigkeit des Moduls bedeutet, dass ein einziger Experte alle notwendigen Daten für die Prüfung und das Prüfsystem selbst, das sie verwendet, enthält. Dieser Experte kann leicht an ein verteiltes Computernetzwerk weitergegeben werden, falls eine Cloud-Optimierung erforderlich ist.
Kommen wir nun zur Beschreibung des ersten Teils des Systems, nämlich der Speicherung der Historie für den Test.
Speichern der historischen Daten eines Symbols für den Tester als Basis für "Mathematischen Berechnung"
Die "Mathematische Berechnung" bietet keinen Zugang zu den Handelsinstrumenten. Der Aufruf von Funktionen wie CopyRates(Symbol(),...) funktioniert hier einfach nicht. Für die Simulation sind jedoch historische Daten notwendig. Zu diesem Zweck kann die Kurshistorie des gewünschten Symbols in einem vorkomprimierten Array vom Typ uchar[] gespeichert werden:
uchar symbol[128394] = {0x98,0x32,0xa6,0xf7,0x64,0xbc...};
Jede Art von Daten — Ton, Bild, Zahlen und Zeichenketten — kann als einfacher Satz von Bytes dargestellt werden. Ein Byte ist ein kurzer Block, der aus acht Bits besteht. Jede Information wird als "Batches" in einer Sequenz aus diesen Bytes gespeichert. MQL5 hat einen speziellen Datentyp — uchar, wobei jeder Wert genau ein Byte repräsentieren kann. So kann ein Array vom Typ uchar mit 100 Elementen 100 Bytes speichern.
Die Kurse eines Symbols bestehen aus vielen Bars. Jede Bar enthält die Informationen über die Öffnungszeit der Bar, ihre Preise (High, Low, Open und Close) und das Volumen. Jeder dieser Werte wird in einer Variablen mit der entsprechenden Länge gespeichert. Hier ist die Tabelle:
Wert | Datentyp | Größe in Bytes |
---|---|---|
Eröffnungszeit | datetime | 8 |
Eröffnung | double | 8 |
Hoch | double | 8 |
Tief | double | 8 |
Schlusskurs | double | 8 |
Tick-Volumen | long | 8 |
Spread | int | 4 |
Real-Volumen | long | 8 |
Es ist einfach zu berechnen, dass das Speichern einer Bar 60 Bytes oder ein Uchar-Array, bestehend aus 60 Elementen, erfordert. Für einen 24-Stunden-Forex-Markt besteht ein Handelstag aus 1.440 Bars von einer Minute. Somit besteht ein Jahr aus ca. 391.680 Bars. Multipliziert man diese Zahl mit 60 Bytes, so stellt man fest, dass die 1-Minuten-Bars eines Jahr in unkomprimierter Form etwa 23 MB beanspruchen. Ist das viel oder wenig? Es ist nicht viel für moderne Standards, aber stellen Sie sich vor, was passiert, wenn wir uns entscheiden, einen Experten für die Daten für 10 Jahre zu testen. Es wird notwendig sein, 230 MB an Daten zu speichern und eventuell sogar über ein Netzwerk zu verteilen. Das ist selbst für moderne Verhältnisse sehr gut.
Daher ist es notwendig, diese Informationen irgendwie zu komprimieren. Glücklicherweise wurde eine spezielle Bibliothek für die Arbeit mit Zip-Archiven geschrieben. Zusätzlich zu den verschiedenen Funktionen ermöglicht diese Bibliothek die Umwandlung des Komprimierungsergebnisses in ein Array von Bytes, was die Arbeit erheblich erleichtert.
Also, unser Algorithmus lädt das MqlRates-Array der Bars, wandelt es in eine Byte-Darstellung um, komprimiert es dann mit dem Zip-Archiv und speichert die komprimierten Daten als ein uchar-Array, das in der mqh-Datei definiert ist.
Um Kurse in ein Byte-Array zu konvertieren, wird das System der Konvertierung über den Typ union verwendet. Dieses System erlaubt es, mehrere Datentypen in einem Speicherfeld zu platzieren. So ist es möglich, auf die Daten eines Typs durch Adressierung eines anderen Typs zuzugreifen. Eine solche union speichert zwei Typen: die MqlRates-Struktur und das uchar-Array, wobei die Anzahl der Elemente der Größe von MqlRates entspricht. Um zu verstehen, wie dieses System funktioniert, verwenden Sie die erste Version des SaveRates.mq5-Skripts, das die Symbolhistorie in ein Uchar-Byte-Array umwandelt:
//+------------------------------------------------------------------+ //| SaveRates.mq5 | //| Copyright 2016, Vasiliy Sokolov. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2016, Vasiliy Sokolov." #property link "http://www.mql5.com" #property version "1.00" #include <Zip\Zip.mqh> #include <ResourceCreator.mqh> input ENUM_TIMEFRAMES MainPeriod; union URateToByte { MqlRates bar; uchar bar_array[sizeof(MqlRates)]; }RateToByte; //+------------------------------------------------------------------+ //| Script Programm Start Funktion | //+------------------------------------------------------------------+ void OnStart() { //-- Laden der Kurse MqlRates rates[]; int total = CopyRates(Symbol(), Period(), 0, 20000, rates); uchar symbol_array[]; //-- Konvertieren in Bytes ArrayResize(symbol_array, sizeof(MqlRates)*total); for(int i = 0, dst = 0; i < total; i++, dst +=sizeof(MqlRates)) { RateToByte.bar = rates[i]; ArrayCopy(symbol_array, RateToByte.bar_array, dst, 0, WHOLE_ARRAY); } //-- Komprimieren in ein Zip-Archive CZip Zip; CZipFile* file = new CZipFile(Symbol(), symbol_array); Zip.AddFile(file); uchar zip_symbol[]; //-- Abfrage der Byte-Darstellung des komprimierten Archivs Zip.ToCharArray(zip_symbol); //-- Schreiben als mhq-Datei zum Laden CCreator creator; creator.ByteArrayToMqhArray(zip_symbol, "rates.mqh", "rates"); } //+------------------------------------------------------------------+
Nachdem dieser Code ausgeführt wurde, enthält das Zip-Array des Symbols, ein komprimiertes Array von MqlRates-Strukturen — die komprimierte Historie der Kurse. Dann wird das komprimierte Array als mqh-Datei auf der Festplatte des Computers gespeichert. Wie und warum dies geschieht, erfahren Sie weiter unten.
Eine Byte-Darstellung der Kurse ist nicht ausreichend, diese zu komprimieren. Es ist notwendig, diese Darstellung als uchar-Array aufzuzeichnen. In diesem Fall sollte das Array in Form einer resource geladen werden, d.h. es muss mit dem Programm kompiliert werden. Erstellen Sie zu diesem Zweck eine spezielle mqh-Header-Datei, die dieses Array als einfachen Satz von ASCII-Zeichen enthält. Verwenden Sie dazu die spezielle Klasse CResourceCreator:
//+------------------------------------------------------------------+ //| ResourceCreator.mqh | //| Copyright 2017, Vasiliy Sokolov. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2017, Vasiliy Sokolov." #property link "http://www.mql5.com" #include <Arrays\ArrayObj.mqh> //+------------------------------------------------------------------+ //| Enthält die String-ID. des erzeugten Ressourcen-Arrays | //+------------------------------------------------------------------+ class CResInfo : public CObject { public: string FileName; string MqhFileName; string ArrayName; }; //+------------------------------------------------------------------+ //| Erstellt eine MQL-resource als Byte-Array. | //+------------------------------------------------------------------+ class CCreator { private: int m_common; bool m_ch[256]; string ToMqhName(string name); void CreateInclude(CArrayObj* list_info, string file_name); public: CCreator(void); void SetCommonDirectory(bool common); bool FileToByteArray(string file_name, uchar& byte_array[]); bool ByteArrayToMqhArray(uchar& byte_array[], string file_name, string array_name); void DirectoryToMqhArray(string src_dir, string dst_dir, bool create_include = false); }; //+------------------------------------------------------------------+ //| Standardkonstruktor | //+------------------------------------------------------------------+ CCreator::CCreator(void) : m_common(FILE_COMMON) { ArrayInitialize(m_ch, false); for(uchar i = '0'; i < '9'; i++) m_ch[i] = true; for(uchar i = 'A'; i < 'Z'; i++) m_ch[i] = true; } //+------------------------------------------------------------------+ //| Setzt FILE_COMMON-Flag, oder entfernt es | //+------------------------------------------------------------------+ CCreator::SetCommonDirectory(bool common) { m_common = common ? FILE_COMMON : 0; } //+------------------------------------------------------------------+ //| Konvertiert alle Dateien im src_dir Verzeichnis in mqh Dateien | //| enthält die Byte-Darstellung dieser Dateien | //+------------------------------------------------------------------+ void CCreator::DirectoryToMqhArray(string src_dir,string dst_dir, bool create_include = false) { string file_name; string file_mqh; CArrayObj list_info; long h = FileFindFirst(src_dir+"\\*", file_name, m_common); if(h == INVALID_HANDLE) { printf("Directory" + src_dir + " is not found, or it does not contain files"); return; } do { uchar array[]; if(FileToByteArray(src_dir+file_name, array)) { string norm_name = ToMqhName(file_name); file_mqh = dst_dir + norm_name + ".mqh"; ByteArrayToMqhArray(array, file_mqh, "m_"+norm_name); printf("Create resource: " + file_mqh); // Hinzufügen von Informationen über die erstellte resource CResInfo* info = new CResInfo(); list_info.Add(info); info.FileName = file_name; info.MqhFileName = norm_name + ".mqh"; info.ArrayName = "m_"+norm_name; } }while(FileFindNext(h, file_name)); if(create_include) CreateInclude(&list_info, dst_dir+"include.mqh"); } //+------------------------------------------------------------------+ /| Erzeugt eine mqh-Datei mit Einbindungen aller generierten Dateien | //+------------------------------------------------------------------+ void CCreator::CreateInclude(CArrayObj *list_info, string file_name) { int handle = FileOpen(file_name, FILE_WRITE|FILE_TXT|m_common); if(handle == INVALID_HANDLE) { printf("Failed to create the include file " + file_name); return; } // Erstellen des Include-Headers for(int i = 0; i < list_info.Total(); i++) { CResInfo* info = list_info.At(i); string line = "#include \"" + info.MqhFileName + "\"\n"; FileWriteString(handle, line); } // Erstellen Sie eine Funktion zum Kopieren des resource Arrays in den aufrufenden Code. FileWriteString(handle, "\n"); FileWriteString(handle, "void CopyResource(string file_name, uchar &array[])\n"); FileWriteString(handle, "{\n"); for(int i = 0; i < list_info.Total(); i++) { CResInfo* info = list_info.At(i); if(i == 0) FileWriteString(handle, " if(file_name == \"" + info.FileName + "\")\n"); else FileWriteString(handle, " else if(file_name == \"" + info.FileName + "\")\n"); FileWriteString(handle, " ArrayCopy(array, " + info.ArrayName + ");\n"); } FileWriteString(handle, "}\n"); FileClose(handle); } //+------------------------------------------------------------------+ //| konvertiert den Namen in einen korrekten Namen der MQL-Variablen | //+------------------------------------------------------------------+ string CCreator::ToMqhName(string name) { uchar in_array[]; uchar out_array[]; int total = StringToCharArray(name, in_array); ArrayResize(out_array, total); int t = 0; for(int i = 0; i < total; i++) { uchar ch = in_array[i]; if(m_ch[ch]) out_array[t++] = ch; else if(ch == ' ') out_array[t++] = '_'; uchar d = out_array[t-1]; int dbg = 4; } string line = CharArrayToString(out_array, 0, t); return line; } //+------------------------------------------------------------------+ //| Rückgabe der Byte-Darstellung der übergebenen Datei | //| als Bytr-Array | //+------------------------------------------------------------------+ bool CCreator::FileToByteArray(string file_name, uchar& byte_array[]) { int handle = FileOpen(file_name, FILE_READ|FILE_BIN|m_common); if(handle == -1) { printf("Failed to open file " + file_name + ". Reason: " + (string)GetLastError()); return false; } FileReadArray(handle, byte_array, WHOLE_ARRAY); FileClose(handle); return true; } //+------------------------------------------------------------------+ //| Konvertiert das Byte-Arrays in eine mqh-Datei mit dem Namen | //| Dateiname mit der Beschreibung eines Arrays mit dem Namen | //| des Arrays | //+------------------------------------------------------------------+ bool CCreator::ByteArrayToMqhArray(uchar& byte_array[], string file_name, string array_name) { int size = ArraySize(byte_array); if(size == 0) return false; int handle = FileOpen(file_name, FILE_WRITE|FILE_TXT|m_common, ""); if(handle == -1) return false; string strSize = (string)size; string strArray = "uchar " +array_name + "[" + strSize + "] = \n{\n"; FileWriteString(handle, strArray); string line = " "; int chaptersLine = 32; for(int i = 0; i < size; i++) { ushort ch = byte_array[i]; line += (string)ch; if(i == size - 1) line += "\n"; if(i>0 && i%chaptersLine == 0) { if(i < size-1) line += ",\n"; FileWriteString(handle, line); line = " "; } else if(i < size - 1) line += ","; } if(line != "") FileWriteString(handle, line); FileWriteString(handle, "};"); FileClose(handle); return true; }
Wir gehen auf die Funktionsweise nicht im Detail ein, sondern beschreiben nur allgemein, indem wir ihre Merkmale aufzählen.
- Liest jede beliebige Datei auf der Festplatte und speichert ihre Byte-Darstellung als uchar-Array in einer mqh-Datei.
- Liest beliebige Verzeichnisse auf dem Festplattenlaufwerk und speichert die Byte-Darstellung aller Dateien, die sich in diesem Verzeichnis befinden. Die Byte-Darstellung für jede solche Datei befindet sich in einer separaten mqh-Datei, die ein uchar-Array enthält.
- Es nimmt das uchar-Array von Bytes als Dateneingabe und speichert es als Array von Zeichen in einer separaten mqh-Datei.
- Erstellt eine spezielle Header-Datei, die Links zu allen mqh-Dateien enthält, die während des Generierungsprozesses erstellt wurden. Außerdem wird eine spezielle Funktion erstellt, die den Array-Namen als Dateneingabe nimmt und dessen Byte-Darstellung zurückgibt. Dieser Algorithmus verwendet dynamische Codegenerierung.
Die beschriebene Klasse ist eine leistungsfähige Alternative zur normalen Mittelanforderung in einem MQL-Programm.
Standardmäßig finden alle Dateioperationen im gemeinsamen Dateiverzeichnis (FILE_COMMON) statt. Wenn Sie das Skript aus der vorherigen Liste ausführen, hat der Ordner eine neue Datei rates.mqh (der Dateiname wird durch den zweiten Parameter der Methode ByteArrayToMqhArray definiert). Es enthält ein Array mit riesigen Raten[] (der Array-Name wird durch den dritten Parameter dieser Methode definiert). Hier ist ein Ausschnitt aus dem Inhalt der Datei:
Abb. 4. Die MqlRates-Kurse als komprimiertes Byte-Array namens rates
Die Datenkompression funktioniert einwandfrei. Ein Jahr unkomprimierter Ein-Minuten-Historie für das EURUSD-Währungspaar benötigt etwa 20 MB, nach der Komprimierung nur 5 MB. Es ist jedoch besser, die Datei rates.mqh nicht im MetaEditor zu öffnen: Ihre Größe ist viel größer als diese Zahl, und der Editor kann einfrieren. Aber keine Sorge. Nach der Kompilierung wird der Text in Bytes konvertiert und die tatsächliche Größe des Programms erhöht sich nur um den tatsächlichen Wert der gespeicherten Informationen, in diesem Fall um 5 MB.
Übrigens kann diese Technik in einem ex5-Programm verwendet werden, um notwendige Informationen jeglicher Art zu speichern, nicht nur die Kurshistorie.
Laden von MqlRates aus einem komprimierten Byte-Array
Nun, da die Historie gespeichert ist, kann sie in jedes MQL-Programm aufgenommen werden, indem man einfach die Include-Direktive am Anfang hinzufügt:
... #include "rates.mqh" ...
Beachten Sie, dass die Datei rates.mqh in das Quellverzeichnis des Programms selbst verschoben werden sollte.
Das einfache Laden der Daten mittels include reicht nicht aus. Es ist auch notwendig, einen Block von Prozeduren zu schreiben, um die Daten in ein normales MqlRates-Array umzukehren. Lassen Sie uns dazu eine spezielle Funktion LoadRates implementieren. Es wird eine Referenz auf ein leeres MqlRates-Array als Eingabe benötigt. Sobald es ausgeführt wird, enthält das Array die konventionellen Kurse von MqlRates, die aus dem komprimierten Array geladen werden. Hier ist der Code für diese Funktion:
//+------------------------------------------------------------------+ //| Mtester.mqh | //| Copyright 2017, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2017, MetaQuotes Software Corp." #property link "http://www.mql5.com" #include <Zip\Zip.mqh> #include "rates.mqh" //+------------------------------------------------------------------+ //| Casting MqlRates auf uchar[] | //+------------------------------------------------------------------+ union URateToByte { MqlRates bar; uchar bar_array[sizeof(MqlRates)]; }; //+------------------------------------------------------------------+ //| Konvertiert komprimierte Daten in ein MqlRates Array mit Kursen | //| Rückgabe der Anzahl der empfangenen Bars, -1 im Falle eines | //| Fehlers | //+------------------------------------------------------------------+ int LoadRates(string symbol_name, MqlRates &mql_rates[]) { CZip Zip; Zip.CreateFromCharArray(rates); CZipFile* file = dynamic_cast<CZipFile*>(Zip.ElementByName(symbol_name)); if(file == NULL) return -1; uchar array_rates[]; file.GetUnpackFile(array_rates); URateToByte RateToBar; ArrayResize(mql_rates, ArraySize(array_rates)/sizeof(MqlRates)); for(int start = 0, i = 0; start < ArraySize(array_rates); start += sizeof(MqlRates), i++) { ArrayCopy(RateToBar.bar_array, array_rates, 0, start, sizeof(MqlRates)); mql_rates[i] = RateToBar.bar; } return ArraySize(mql_rates); } //+------------------------------------------------------------------+
Die Funktion befindet sich in der Datei Mtester.mqh. Dies ist die erste Funktion, um mit der "Mathematischen Berechnung" zu arbeiten. Neue Funktionen werden bei Bedarf der Datei Mtester.mqh hinzugefügt, und sie kann sich so in eine vollwertige mathematische Testmaschine für Strategien verwandeln.
Schreiben wir eine einfache Strategie für die "Mathematischen Berechnung". Sie führt nur zwei Funktionen aus: Laden der Kurse in der Funktion OnInit und Berechnen des Durchschnittswertes aller Schlusskurse in der Funktion OnTester. Das Ergebnis der Berechnung wird an MetaTrader zurückgegeben:
//+------------------------------------------------------------------+ //| MExpert.mq5 | //| Copyright 2017, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2017, MetaQuotes Software Corp." #property link "http://www.mql5.com" #property version "1.00" #include "Mtester.mqh" //+------------------------------------------------------------------+ //| Kurse der Tests | //+------------------------------------------------------------------+ MqlRates Rates[]; //+------------------------------------------------------------------+ //| Initialisierungsfunktion des Experten | //+------------------------------------------------------------------+ int OnInit() { //-- Lädt die Kurse für das angegebene Symbol. if(LoadRates(Symbol(), Rates)==-1) { printf("Quotes for symbol " + Symbol() + " not found. Create the appropriate quotes resource."); return INIT_PARAMETERS_INCORRECT; } printf("Loaded " + (string)ArraySize(Rates) + " bars for symbol " + Symbol()); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Tester function | //+------------------------------------------------------------------+ double OnTester() { double mean = 0.0; for(int i = 0; i < ArraySize(Rates); i++) mean += Rates[i].close; mean /= ArraySize(Rates); return mean; } //+------------------------------------------------------------------+
Nachdem der Expert Advisor erstellt wurde, laden Sie ihn in den Strategie-Tester und wählen Sie den Modus "Mathematische Berechnung". Führen Sie den Test durch und öffnen Sie das Journal:
2017.12.13 15:12:25.127 Core 2 math calculations test of Experts\MTester\MExpert.ex5 started 2017.12.13 15:12:25.127 Core 2 Loaded 354159 bars for symbol EURUSD 2017.12.13 15:12:25.127 Core 2 OnTester result 1.126596405653942 2017.12.13 15:12:25.127 Core 2 EURUSD,M15: mathematical test passed in 0:00:00.733 (total tester working time 0:00:01.342) 2017.12.13 15:12:25.127 Core 2 217 Mb memory used
Wie Sie sehen können, funktionierte der EA wie erwartet. Alle Kurse wurden korrekt geladen, wie aus dem Datensatz über die Anzahl der geladenen Bars hervorgeht. Außerdem iterierte er korrekt über alle Bars, um den Mittelwert zu berechnen, der an den aufrufenden Thread zurückgegeben wurde. Der Durchschnittspreis aller EURUSD-Notierungen für das vergangene Jahr lag bei 1,12660.
Beispielstrategie auf der Basis eines gleitenden Mittelwerts
Die erzielten Ergebnisse sind beeindruckend: Daten wurden empfangen und komprimiert, dann als statisches uchar-Array gespeichert, das in den Experten geladen wurde, dann wurden die Daten entpackt und wieder in ein Array von Kursen umgewandelt. Jetzt ist es an der Zeit, die erste brauchbare Strategie zu schreiben. Lassen Sie uns die klassische Version verwenden, die auf dem Kreuzen zweier gleitender Durchschnitte basiert. Diese Strategie ist einfach umzusetzen. Da die Handelsumgebung bei der "Mathematischen Berechnung" nicht verfügbar ist, können Indikatoren wie iMA nicht direkt aufgerufen werden. Stattdessen muss der Wert des gleitenden Durchschnitts manuell berechnet werden. Die Hauptaufgabe in diesem Testmodus ist die maximale Beschleunigung. Daher müssen alle verwendeten Algorithmen schnell arbeiten. Es ist bekannt, dass sich die Berechnung des gleitenden Durchschnitts auf die Klasse der einfachen Probleme mit einer Rechenkomplexität von O(1) bezieht. Das bedeutet, dass die Geschwindigkeit der Berechnung des Mittelwertes nicht von der Periodenlänge des gleitenden Durchschnitts abhängen sollte. Zu diesem Zweck wird eine vorgefertigte Bibliothek für den Ringpuffer verwendet. Die Details dieses Algorithmus wurden in einem eigenen Artikel besprochen.
Erstellen wir zunächst eine Vorlage des ersten Experten:
//+------------------------------------------------------------------+ //| MExpert.mq5 | //| Copyright 2017, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2017, MetaQuotes Software Corp." #property link "http://www.mql5.com" #property version "1.00" #include "Mtester.mqh" #include <RingBuffer\RiSma.mqh> input int PeriodFastMa = 9; input int PeriodSlowMa = 16; CRiSMA FastMA; // Ringpuffer zur Berechnung des schnellen gleitenden Mittelwerts CRiSMA SlowMA; // Ringpuffer zur Berechnung des langsamen gleitenden Mittelwerts //+------------------------------------------------------------------+ //| Kurse der Tests | //+------------------------------------------------------------------+ MqlRates Rates[]; //+------------------------------------------------------------------+ //| Initialisierungsfunktion des Experten | //+------------------------------------------------------------------+ int OnInit() { //-- Überprüfen der Korrektheit der Parameterkombination //-- Der schnelle gleitende Durchschnitt darf nicht kleiner sein als der langsame. if(PeriodFastMa >= PeriodSlowMa) return INIT_PARAMETERS_INCORRECT; //-- Initialisieren des Ringpuffers FastMA.SetMaxTotal(PeriodFastMa); SlowMA.SetMaxTotal(PeriodSlowMa); //-- Lädt die Kurse für das angegebene Symbol. if(LoadRates(Symbol(), Rates)==-1) { printf("Quotes for symbol " + Symbol() + " not found. Create the appropriate quotes resource."); return INIT_FAILED; } printf("Loaded " + (string)ArraySize(Rates) + " bars for symbol " + Symbol()); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Beschreibung der Strategie | //+------------------------------------------------------------------+ double OnTester() { for(int i = 1; i < ArraySize(Rates); i++) { FastMA.AddValue(Rates[i].close); SlowMA.AddValue(Rates[i].close); // Die EA-Logik befindet sich hier } return 0.0; } //+------------------------------------------------------------------+
Er definiert zwei Parameter mit den Periodenlängen des schnellen und langsamen MAs. Dann werden zwei Ringspeicher für die Berechnung dieser Mittelwerte deklariert. Der Initialisierungsblock überprüft die Richtigkeit der eingegebenen Parameter. Da die Parameter nicht vom Anwender gesetzt, sondern vom Strategietester im Optimierungsmodus automatisch ausgewählt werden, werden die Parameter oft nicht richtig kombiniert. In diesem Fall kann der schnelle MA langsamer sein als der langsame MA. Um diese Verwirrung zu vermeiden und Zeit bei der Optimierung zu sparen, werden solche Durchläufe bereits vor dem Start beendet. Dies geschieht durch die Rückgabe der Konstante INIT_PARAMETERS_INCORRECT im OnInit-Block.
Nachdem die Puffer initialisiert, Parameter überprüft und die Kurse geladen wurden, können wir den Test jetzt durchzuführen: Die OnTester-Funktion wird gestartet. In ihr befindet sich die Hauptprüfung innerhalb des 'for'-Blocks. Der Code zeigt, dass, wenn der Mittelwert des FastMA-Ringpuffers größer als der Mittelwert von SlowMA ist, eine Kaufposition eröffnet wird. Allerdings gibt es noch kein Handelsmodul für die Eröffnung solcher Kauf- und Verkaufspositionen. Das muss noch geschrieben werden.
Klasse virtueller Positionen
Wie bereits erwähnt, ist die "Mathematische Berechnung" für die Berechnung von Strategien ungeeignet. Daher hat sie keine Handelsfunktionen. Auch die MetaTrader-Umgebung kann nicht verwendet werden. Der Begriff "Position" ist völlig bedeutungslos, er existiert einfach nicht. Daher ist es notwendig, ein vereinfachtes Analogon der MetaTrader-Position zu erstellen. Sie enthält nur die notwendigsten Informationen. Legen Sie dazu eine Klasse mit diesen Feldern an:
- Positions, Eröffnungszeit;
- Position, Eröffnungspreis;
- Position, Schließzeitpunkt;
- Position Schlusskurs;
- Position Volumen;
- Spreizung (spread) im Moment der Positionsöffnung;
- Richtung der Position.
Vielleicht kommen in der Zukunft weitere Felder dazu, aber diese reichen vorerst aus.
//+------------------------------------------------------------------+ //| Mtester.mqh | //| Copyright 2017, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2017, MetaQuotes Software Corp." #property link "http://www.mql5.com" #include <Object.mqh> #include "rates.mqh" #include "Type2Char.mqh" //+------------------------------------------------------------------+ //| Klasse virtueller Positionen für die "Mathematische Berechnung" | //+------------------------------------------------------------------+ class CMposition : public CObject { private: datetime m_time_open; datetime m_time_close; double m_price_open; double m_price_close; double m_volume; int m_spread; ENUM_POSITION_TYPE m_type; public: CMposition(void); static int Sizeof(void); bool IsActive(void); datetime TimeOpen(void); datetime TimeClose(void); double PriceOpen(void); double PriceClose(void); double Volume(void); double Profit(void); ENUM_POSITION_TYPE PositionType(void); static CMposition* CreateOnBarOpen(MqlRates& bar, ENUM_POSITION_TYPE pos_type, double vol); void CloseOnBarOpen(MqlRates& bar); }; //+------------------------------------------------------------------+ //| Eine CMPosition nimmt 45 Byte Daten auf | //+------------------------------------------------------------------+ int CMposition::Sizeof(void) { return 48; } CMposition::CMposition(void):m_time_open(0), m_time_close(0), m_price_open(0.0), m_price_close(0.0), m_volume(0.0) { } //+------------------------------------------------------------------+ //| Wahr, wenn die Position geschlossen ist | //+------------------------------------------------------------------+ bool CMposition::IsActive() { return m_time_close == 0; } //+------------------------------------------------------------------+ //| Zeitpunkt der Eröffnung einer Position | //+------------------------------------------------------------------+ datetime CMposition::TimeOpen(void) { return m_time_open; } //+------------------------------------------------------------------+ //| Position, Schließzeitpunkt | //+------------------------------------------------------------------+ datetime CMposition::TimeClose(void) { return m_time_close; } //+------------------------------------------------------------------+ //| Position, Eröffnungspreis | //+------------------------------------------------------------------+ double CMposition::PriceOpen(void) { return m_price_open; } //+------------------------------------------------------------------+ //| Position Schlusskurs | //+------------------------------------------------------------------+ double CMposition::PriceClose(void) { return m_price_close; } //+------------------------------------------------------------------+ //| Position, Volumen | //+------------------------------------------------------------------+ double CMposition::Volume(void) { return m_volume; } //+------------------------------------------------------------------+ //| Rückgabe des Typs der Position | //+------------------------------------------------------------------+ ENUM_POSITION_TYPE CMposition::PositionType(void) { return m_type; } //+------------------------------------------------------------------+ //| Position, Gewinn | //+------------------------------------------------------------------+ double CMposition::Profit(void) { if(IsActive()) return 0.0; int sign = m_type == POSITION_TYPE_BUY ? 1 : -1; double pips = (m_price_close - m_price_open)*sign; double profit = pips*m_volume; return profit; } //+------------------------------------------------------------------+ //| Position basierend auf den übergebenen Parametern erzeugen | //+------------------------------------------------------------------+ static CMposition* CMposition::CreateOnBarOpen(MqlRates &bar, ENUM_POSITION_TYPE pos_type, double volume) { CMposition* position = new CMposition(); position.m_time_open = bar.time; position.m_price_open = bar.open; position.m_volume = volume; position.m_type = pos_type; return position; } //+------------------------------------------------------------------+ //| Handelsmodul zur Eröffnung virtueller Positionen | //+------------------------------------------------------------------+ void CMposition::CloseOnBarOpen(MqlRates &bar) { m_price_close = bar.open; m_time_close = bar.time; } //+------------------------------------------------------------------+
Das Eröffnen einer Position ist der interessanteste Punkt in dieser Implementierung. Die Felder der Klasse sind vor externen Modifikationen geschützt, aber die statische Methode CreateOnBarOpen gibt das Klassenobjekt mit korrekt gesetzten Parametern zurück. Es gibt keine Möglichkeit, ein Objekt dieser Klasse ohne Bezug auf diese Methode anzulegen. Dadurch werden die Daten vor unbeabsichtigten Änderungen geschützt.
Klasse für den Handel
Es ist nun notwendig, eine Klasse für die Verwaltung dieser Bestände anzulegen. Es wird das Analogon der MetaTrader-Funktionen sein. Selbstverständlich sollten auch die Positionen selbst in diesem Modul gespeichert werden. Zu diesem Zweck sind zwei Objekte des Typs CArrayObj vorgesehen: Die erste — Active — wird für die Speicherung der offenen Positionen der Strategie benötigt, die zweite — History — enthält die geschlossenen Positionen.
Die Klasse wird auch über spezielle Methoden zum Öffnen und Schließen von Positionen verfügen:
- EntryAtOpenBar — öffnet eine Position in angegebenen Richtung und Volumen;
- CloseAtOpenBar — schließt die Position an dem angegebenen Index.
Die Positionen werden zu den Preisen der durchlaufenen Bar geöffnet und geschlossen. Leider verhindert dieser Ansatz nicht den "Blick in die Zukunft", aber er ist einfach zu implementieren und sehr schnell.
Die Klasse CMtrade (nennen wir es so) erweist sich als recht einfach:
//+------------------------------------------------------------------+ //| Mtester.mqh | //| Copyright 2017, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2017, MetaQuotes Software Corp." #property link "http://www.mql5.com" #include <Object.mqh> #include <Arrays\ArrayObj.mqh> #include "Mposition.mqh" //+------------------------------------------------------------------+ //| Handelsmodul zur Eröffnung virtueller Positionen | //+------------------------------------------------------------------+ class CMtrade { public: CMtrade(void); ~CMtrade(); CArrayObj Active; CArrayObj History; void EntryAtOpenBar(MqlRates &bar, ENUM_POSITION_TYPE pos_type, double volume); void CloseAtOpenBar(MqlRates &bar, int pos_index); }; //+------------------------------------------------------------------+ //| Standardkonstruktor | //+------------------------------------------------------------------+ CMtrade::CMtrade(void) { Active.FreeMode(false); } //+------------------------------------------------------------------+ //| Löschen der verbliebenen Positionen | //+------------------------------------------------------------------+ CMtrade::~CMtrade() { Active.FreeMode(true); Active.Clear(); } //+------------------------------------------------------------------+ //| Erzeugt eine neue Position und fügt sie zur Liste der aktiven | //| Positionen. | //+------------------------------------------------------------------+ void CMtrade::EntryAtOpenBar(MqlRates &bar, ENUM_POSITION_TYPE pos_type, double volume) { CMposition* pos = CMposition::CreateOnBarOpen(bar, pos_type, volume); Active.Add(pos); } //+------------------------------------------------------------------+ //| Schließt eine virtuelle Position mit Index pos_index zum | //| Eröffnungspreis der letzten Bar | //+------------------------------------------------------------------+ void CMtrade::CloseAtOpenBar(MqlRates &bar, int pos_index) { CMposition* pos = Active.At(pos_index); pos.CloseOnBarOpen(bar); Active.Delete(pos_index); History.Add(pos); } //+------------------------------------------------------------------+
In der Praxis reduziert sich die gesamte Funktionalität auf zwei Funktionen:
- Es wird eine neue Position von der Methode CMposition::CreateOnBarOpen übernommen und in die Liste Active (Methode EntryOnOpenBar) eingetragen;
- Verschieben der ausgewählten Position von der Liste der offenen Positionen in die Liste der geschlossenen Positionen, nachdem diese Position durch die statische Methode CMposition::CLoseOnBarOpen geschlossen wurde.
Die Klasse für den Handel wurde erstellt, und alle Komponenten zum Testen des Experten sind jetzt verfügbar.
Die erste Prüfung des Expert Advisors. Optimieren
Lassen Sie uns alle Komponenten zusammenfügen. Hier ist der Quellcode der Strategie, der auf zwei gleitenden Durchschnitten für die Arbeit im mathematischen Optimierer basiert.
//+------------------------------------------------------------------+ //| MExpert.mq5 | //| Copyright 2017, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2017, MetaQuotes Software Corp." #property link "http://www.mql5.com" #property version "1.00" #include <RingBuffer\RiSma.mqh> #include "Mtester.mqh" input int PeriodFastMa = 9; input int PeriodSlowMa = 16; CRiSMA FastMA; // Ringpuffer zur Berechnung des schnellen gleitenden Mittelwerts CRiSMA SlowMA; // Ringpuffer zur Berechnung des langsamen gleitenden Mittelwerts CMtrade Trade; // Handelsmodul für virtuelle Berechnungen //+------------------------------------------------------------------+ //| Kurse der Tests | //+------------------------------------------------------------------+ MqlRates Rates[]; //+------------------------------------------------------------------+ //| Initialisierungsfunktion des Experten | //+------------------------------------------------------------------+ int OnInit() { //-- Überprüfen der Korrektheit der Parameterkombination //-- Der schnelle gleitende Durchschnitt darf nicht kleiner sein als der langsame. //if(PeriodFastMa >= PeriodSlowMa) // return INIT_PARAMETERS_INCORRECT; //-- Initialisieren des Ringpuffers FastMA.SetMaxTotal(PeriodFastMa); SlowMA.SetMaxTotal(PeriodSlowMa); //-- Lädt die Kurse für das angegebene Symbol. if(LoadRates(Symbol(), Rates)==-1) { printf("Quotes for symbol " + Symbol() + " not found. Create the appropriate quotes resource."); return INIT_FAILED; } printf("Loaded " + (string)ArraySize(Rates) + " bars for symbol " + Symbol()); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Beschreibung der Strategie | //+------------------------------------------------------------------+ double OnTester() { for(int i = 1; i < ArraySize(Rates)-1; i++) { MqlRates bar = Rates[i]; FastMA.AddValue(Rates[i].close); SlowMA.AddValue(Rates[i].close); ENUM_POSITION_TYPE pos_type = FastMA.SMA() > SlowMA.SMA() ? POSITION_TYPE_BUY : POSITION_TYPE_SELL; //-- Schließen der Positionen, die dem aktuellen Signal entgegengesetzt sind. for(int k = Trade.Active.Total()-1; k >= 0 ; k--) { CMposition* pos = Trade.Active.At(k); if(pos.PositionType() != pos_type) Trade.CloseAtOpenBar(Rates[i+1], k); } //-- Wenn es keine Positionen gibt, wird eine neue Position in der angegebenen Richtung eröffnet. if(Trade.Active.Total() == 0) Trade.EntryAtOpenBar(Rates[i+1], pos_type, 1.0); } double profit = 0.0; for(int i = 0; i < Trade.History.Total(); i++) { CMposition* pos = Trade.History.At(i); profit += pos.Profit(); } return profit; } //+------------------------------------------------------------------+
Die Funktion OnTester ist damit komplett. Der Code ist sehr einfach. Betrachten wir seine Funktionsweise Schritt für Schritt.
- Eine 'for'-Schleife iteriert über alle Kurse.
- Die aktuelle Richtung des Geschäfts wird innerhalb der Schleife bestimmt: Kaufen, wenn der schnelle SMA über dem langsamen SMA steigt, und verkaufen im umgekehrten Falle.
- Alle offenen Positionen werden durchlaufen, und wenn ihre Richtung nicht mit der aktuellen Richtung übereinstimmt, werden sie geschlossen.
- Wenn es keine Positionen gibt, wird eine neue Position in der angegebenen Richtung geöffnet.
- Am Ende der Suche wird noch einmal über alle geschlossenen Positionen iteriert und den Gesamtgewinn zu berechnen, der später an den Strategietester zurückgegeben wird.
Der Experte ist nun bereit zum Testen im Optimierer. Führen Sie es einfach mit der "Mathematischen Berechnung" durch. Um sicherzustellen, dass die Optimierung funktioniert, lassen Sie uns eine vollständige Suche nach den Parametern des gleitenden Mittelwerts durchführen, wie in der folgenden Abbildung gezeigt:
Abb. 5. Auswahl der Parameter für die Optimierung
Das vorliegende Beispiel zeigt 1000 Optimierungsdurchläufe, die jeweils die 1-Minuten-Bars eines Jahres verarbeiten. Trotzdem nehmen die Berechnungen in diesem Modus nicht viel Zeit in Anspruch. Auf einem Computer mit einem i7-Prozessor dauerte die gesamte Optimierung ca. 1 Minute, woraufhin ein Diagramm erstellt wurde:
Abb. 6. Das Diagramm nach 1000 Durchläufen im Modus "langsame Optimierung".
Aber bisher gibt es nur sehr wenige Werkzeuge für die Analyse der erzielten Ergebnisse. In der Tat, alles, was wir derzeit haben, ist eine einzige Zahl, die den virtuellen Gewinn widerspiegelt. Um diese Situation zu beheben, ist es notwendig, ein eigenes Optimierungsdatenformat zu entwickeln und einen Mechanismus zur Generierung und zum Laden zu entwickeln. Darauf werden wir weiter unten eingehen.
Speichern der benutzerdefinierten Optimierungsergebnissen mit Hilfe des Frame-Mechanismus
MetaTrader 5 implementiert eine sehr fortschrittliche Technik für die Arbeit mit benutzerdefinierten Daten. Es basiert auf dem Mechanismus zur Generierung und zum Abruf von sogenannten Frames. Es handelt sich im Wesentlichen um gewöhnliche Binärdaten, die entweder als separate Werte oder als Array dieser Werte existieren. Beispielsweise ist es möglich, während der Optimierung ein Array von Daten beliebiger Größe zu generieren und an den Strategietester des MetaTrader 5 zu übergeben. Die in diesem Array enthaltenen Daten können mit der Funktion FrameNext ausgelesen und weiterverarbeitet werden, z.B. als Bildschirmanzeige. Die Arbeit mit Frames ist nur im Optimierungsmodus und nur innerhalb von drei Funktionen möglich: OnTesterInit(), OnTesterDeinit() und OnTesterPass(). Sie alle haben keine Parameter und keine Rückgabewerte. Aber alles ist einfacher, als es den Anschein hat. Um dies zu veranschaulichen, lassen Sie uns ein einfaches Skript erstellen, das den allgemeinen Algorithmus für die Arbeit mit Frames zeigt:
//+------------------------------------------------------------------+ //| OnTesterSample.mq5 | //| Copyright 2017, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2017, MetaQuotes Software Corp." #property link "http://www.mql5.com" #property version "1.00" input int Param = 1; //+------------------------------------------------------------------+ //| OnTesterInit function | //+------------------------------------------------------------------+ void OnTesterInit() { printf("Optimization started"); } //+------------------------------------------------------------------+ //| Ein Durchlauf der Strategie findet hier statt | //+------------------------------------------------------------------+ double OnTester() { uchar data[5] = {1,2,3,4,5}; // Generieren Sie die Daten für den Frame< FrameAdd("sample", 1, Param, data); // Erstellen eines neuen Frames mit den übergebenen Daten return 3.0; } //+------------------------------------------------------------------+ //| Der letzte Optimierungs-Frame kann hier abgerufen werden | //+------------------------------------------------------------------+ void OnTesterPass() { ulong pass = 0; string name = ""; ulong id = 0; double value = 0.0; uchar data[]; FrameNext(pass, name, id, value, data); //-- Erstellen der Datei des Durchlaufs und Integration in das Zip-Archiv hinzu printf("Received new frame # " + (string)pass + ". Name: " + (string)name + " ID: " + (string)id + " Value of Param: " + DoubleToString(value, 0)); } //+------------------------------------------------------------------+ //| OnTesterDeinit function | //+------------------------------------------------------------------+ void OnTesterDeinit() { printf("Optimization complete"); } //+------------------------------------------------------------------+
Führen Sie diesen Code im Strategie-Tester mit der "Mathematischen Berechnung" aus. Stellen Sie als Optimierungsmodus "Langsame Optimierung" ein. Der einzige Parameter Param wird im Bereich von 10 bis 90 mit einer Schrittweite von 10 geändert.
Nachrichten über den Empfang neuer Frames werden sofort nach dem Start der Optimierung angezeigt. Der Beginn und das Ende der Optimierung werden ebenfalls durch spezielle Ereignisse verfolgt. Anwendungsprotokoll:
2017.12.19 16:58:08.101 OnTesterSample (EURUSD,M15) Optimization started 2017.12.19 16:58:08.389 OnTesterSample (EURUSD,M15) Received new frame # 1. Name: sample ID: 1 Value of Param: 20 2017.12.19 16:58:08.396 OnTesterSample (EURUSD,M15) Received new frame # 0. Name: sample ID: 1 Value of Param: 10 2017.12.19 16:58:08.408 OnTesterSample (EURUSD,M15) Received new frame # 4. Name: sample ID: 1 Value of Param: 50 2017.12.19 16:58:08.426 OnTesterSample (EURUSD,M15) Received new frame # 5. Name: sample ID: 1 Value of Param: 60 2017.12.19 16:58:08.426 OnTesterSample (EURUSD,M15) Received new frame # 2. Name: sample ID: 1 Value of Param: 30 2017.12.19 16:58:08.432 OnTesterSample (EURUSD,M15) Received new frame # 3. Name: sample ID: 1 Value of Param: 40 2017.12.19 16:58:08.443 OnTesterSample (EURUSD,M15) Received new frame # 6. Name: sample ID: 1 Value of Param: 70 2017.12.19 16:58:08.444 OnTesterSample (EURUSD,M15) Received new frame # 7. Name: sample ID: 1 Value of Param: 80 2017.12.19 16:58:08.450 OnTesterSample (EURUSD,M15) Received new frame # 8. Name: sample ID: 1 Value of Param: 90 2017.12.19 16:58:08.794 OnTesterSample (EURUSD,M15) Optimization complete
Am interessantesten sind die Meldungen, die die Informationen über die Nummer des Frames, deren Bezeichner und den Wert des Parameters Param anzeigen. All diese wertvollen Informationen können mit der Funktion FrameNext abgerufen werden.
Eine interessante Eigenschaft dieses Modus ist der Doppelstart des Experten. Ein Experte mit Ereignisbehandlung in seinem Code wird zweimal gestartet: Zuerst im Strategie-Optimierer, dann im Chart in Echtzeit. Während der Experte im Optimierer neue Daten generiert, erhält der Experte, der auf dem Chart läuft, diese. So werden die Quelltexte des Experten von verschiedenen Instanzen des Experten bearbeitet, auch wenn sie sich an derselben Stelle befinden.
Sobald die Daten in der Funktion OnTesterPass empfangen wurden, können sie beliebig weiterverarbeitet werden. Im Testmuster werden diese Daten einfach über die printf-Funktion auf der Konsole ausgegeben. Die zu implementierende Datenverarbeitung kann sich jedoch als wesentlich komplizierter erweisen. Dies wird im nächsten Abschnitt behandelt.
Erhalten einer Byte-Darstellung der Positionshistorie. Speichern von Daten in Frames
Der Frame-Mechanismus bietet eine komfortable Möglichkeit, Informationen zu speichern, zu verarbeiten und zu verteilen. Es ist jedoch notwendig, diese Informationen selbst zu generieren. Im Beispiel-Array war es ein einfaches statisches uchar-Array mit den Werten 1, 2, 3, 3, 4, 5. Solche Daten sind nicht sehr nützlich. Aber ein Byte-Array kann eine beliebige Länge haben und beliebige Daten speichern. Dazu sollten die benutzerdefinierten Datentypen in ein Byte-Array vom Typ uchar konvertiert werden. Etwas Ähnliches wurde bereits mit MqlRates gemacht, wo die Kurse in einem Byte-Array gespeichert wurden. Das gleiche gilt für die benutzerdefinierten Daten.
Ein individueller Strategietester Tester besteht aus zwei Teilen. Der erste Teil generiert Daten, der zweite Teil analysiert die Daten und stellt sie in einer benutzerfreundlichen Form dar. Es ist auch offensichtlich, dass die wichtigsten Informationen für die Strategie-Analyse erhalten werden können, indem alle geschlossenen Positionen analysiert werden. Daher werden am Ende jedes Laufs alle geschlossenen Positionen in ein Byte-Array umgewandelt, das später dem neuen Frame hinzugefügt wird. Nach dem Empfang eines solchen Frames in der OnTesterPass() Funktion ist es möglich, die zuvor empfangenen Frames hinzuzufügen und so eine ganze Sammlung von Frames zu erzeugen.
Es wird notwendig sein, nicht nur die Daten über die Positionen in ein Byte-Array umzuwandeln, sondern auch deren Daten zu extrahieren. Dies erfordert zwei Prozeduren für jeden Datentyp:
- Verfahren zur Umwandlung eines benutzerdefinierten Typs in ein Byte-Array;
- Verfahren zur Umwandlung eines Byte-Arrays in einen benutzerdefinierten Typ.
Wir haben ja bereits das Handelsmodul CMtrade mit zwei Objekttypen für die Positionen — offen und geschlossen. Konzentrieren wir uns auf die geschlossenen Positionen. Die Prozeduren zur Konvertierung virtueller Positionen werden als entsprechende Methoden geschrieben.
Die Methode zur Umwandlung einer Position in ein Byte-Array:
//+------------------------------------------------------------------+ //| Konvertiert eine Position in eine Byte-Darstellung in einen Array| //+------------------------------------------------------------------+ int CMposition::ToCharArray(int dst_start, uchar &array[]) { int offset = dst_start; //-- Kopieren der Eröffnungszeit der Position type2char.time_value = m_time_open; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(datetime)); offset += sizeof(datetime); //-- Kopieren des Zeitpunktes der Schließung der Position type2char.time_value = m_time_close; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(datetime)); offset += sizeof(datetime); //-- Kopieren des Eröffnungspreises der Position type2char.double_value = m_price_open; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(double)); offset += sizeof(double); //-- Kopieren des Schlusskurses der Position type2char.double_value = m_price_close; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(double)); offset += sizeof(double); //-- Kopieren des Volumens der Position type2char.double_value = m_volume; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(double)); offset += sizeof(double); //-- Kopieren des Spreads des Symbols type2char.int_value = m_spread; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(int)); offset += sizeof(int); //-- Kopieren des Typs der Position type2char.int_value = m_type; ArrayCopy(array, type2char.char_array, offset, 0, sizeof(char)); offset += sizeof(int); //-- Rückgabe des letzten Offsets return offset; }
Die umgekehrte Prozedur:
//+------------------------------------------------------------------+ //| Lädt eine Position aus einem Byte-Array | //+------------------------------------------------------------------+ int CMposition::FromCharArray(int dst_start, uchar &array[]) { int offset = dst_start; //-- Kopieren der Eröffnungszeit der Position ArrayCopy(type2char.char_array, array, 0, offset, sizeof(datetime)); m_time_open = type2char.time_value; offset += sizeof(datetime); //-- Kopieren des Zeitpunktes der Schließung der Position ArrayCopy(type2char.char_array, array, 0, offset, sizeof(datetime)); m_time_close = type2char.time_value; offset += sizeof(datetime); //-- Kopieren des Eröffnungspreises der Position ArrayCopy(type2char.char_array, array, 0, offset, sizeof(double)); m_price_open = type2char.double_value; offset += sizeof(double); //-- Kopieren des Schlusskurses der Position ArrayCopy(type2char.char_array, array, 0, offset, sizeof(double)); m_price_close = type2char.double_value; offset += sizeof(double); //-- Kopieren des Volumens der Position ArrayCopy(type2char.char_array, array, 0, offset, sizeof(double)); m_volume = type2char.double_value; offset += sizeof(double); //-- Kopieren des Spreads des Symbols ArrayCopy(type2char.char_array, array, 0, offset, sizeof(int)); m_spread = type2char.int_value; offset += sizeof(int); //-- Kopieren des Typs der Position ArrayCopy(type2char.char_array, array, 0, offset, sizeof(int)); m_type = (ENUM_POSITION_TYPE)type2char.int_value; offset += sizeof(int); //-- Rückgabe des letzten Offsets return offset; }
Die union TypeToChar (mittels der Instanz type2char) ist der Kern für beide Konvertierungen:
//+------------------------------------------------------------------+ //| Konvertieren einfacher Typen in ein Byte-Array | //+------------------------------------------------------------------+ union TypeToChar { uchar char_array[128]; int int_value; double double_value; float float_value; long long_value; short short_value; bool bool_value; datetime time_value; char char_value; };
Alles ist ähnlich wie bei der union RateToByte, die im Abschnitt über die Konvertierung von Kursen besprochen wird.
Alle Prozeduren sind so konzipiert, dass sie das Laden von Daten aus dem globalen Array mit den Daten aller geschlossenen virtuellen Positionen ermöglichen. Dies ermöglicht die Organisation eines hocheffizienten, erschöpfenden Algorithmus, der kein zusätzliches Kopieren des Speichers erfordert.
Die CMTrade-Klasse durchläuft alle geschlossenen Positionen. Das ist logisch, wenn man bedenkt, dass es derjenige ist, der die Sammlung geschlossener Positionen speichert. Die Klasse, ähnlich wie CMposition, arbeitet in zwei Richtungen: wandelt eine Gruppe von geschlossener Positionen in ein Uchar-Array um und führt auch die umgekehrte Prozedur durch: Laden der Liste geschlossenen Positionen aus einem Byte-Array.
Die Prozedur zur Umwandlung einer Liste in ein Byte-Array:
//+------------------------------------------------------------------+ //| Konvertiert die Liste geschl. Positionen in ein Zip-Archiv | //| als Byte-Array. Rückgabe von wahr im Erfolgsfall | //| sonst falsch. | //+------------------------------------------------------------------+ bool CMtrade::ToCharArray(uchar &array[]) { int total_size = CMposition::Sizeof()*History.Total(); if(total_size == 0) { printf(__FUNCTION__ + ": Received array is empty"); return false; } if(ArraySize(array) != total_size && ArrayResize(array, total_size) != total_size) { printf(__FUNCTION__ + ": failed resized received array"); return false; } //-- Speichern der Position in einem Byte-Stream for(int offset = 0, i = 0; offset < total_size; i++) { CMposition* pos = History.At(i); offset = pos.ToCharArray(offset, array); } return true; }
Die umgekehrte Prozedur:
//+------------------------------------------------------------------+ //| Lädt Liste historischer Positionen aus komprimiertem Zip-Archiv | //| übergeben als Byte-Array. Rückgabe von wahr im Erfolgsfall, | //| sonst falsch. | //+------------------------------------------------------------------+ bool CMtrade::FromCharArray(uchar &array[], bool erase_prev_pos = true) { if(ArraySize(array) == 0) { printf(__FUNCTION__ + ": Received array is empty"); return false; } //-- Die Größe eines Byte-Streams muss exakt mit der Byte-Darstellung der Positionen übereinstimmen. int pos_total = ArraySize(array)/CMposition::Sizeof(); if(ArraySize(array)%CMposition::Sizeof() != 0) { printf(__FUNCTION__ + ": Wrong size of received array"); return false; } if(erase_prev_pos) History.Clear(); //-- Wiederherstellung aller Positionen aus dem Byte-Stream. for(int offset = 0; offset < ArraySize(array);) { CMposition* pos = new CMposition(); offset = pos.FromCharArray(offset, array); History.Add(pos); } return History.Total() > 0; }
Um alle Elemente zusammenzufügen, genügt es, die Byte-Darstellung der geschlossenen Positionen am Ende des Durchlaufs zu übernehmen und sie im Frame zu speichern:
//+------------------------------------------------------------------+ //| Beschreibung der Strategie | //+------------------------------------------------------------------+ double OnTester() { for(int i = 1; i < ArraySize(Rates)-1; i++) { MqlRates bar = Rates[i]; FastMA.AddValue(Rates[i].close); SlowMA.AddValue(Rates[i].close); ENUM_POSITION_TYPE pos_type = FastMA.SMA() > SlowMA.SMA() ? POSITION_TYPE_BUY : POSITION_TYPE_SELL; //-- Schließen der Positionen, die dem aktuellen Signal entgegengesetzt sind. for(int k = Trade.Active.Total()-1; k >= 0 ; k--) { CMposition* pos = Trade.Active.At(k); if(pos.PositionType() != pos_type) Trade.CloseAtOpenBar(Rates[i+1], k); } //-- Wenn es keine Positionen gibt, wird eine neue Position in der angegebenen Richtung eröffnet. if(Trade.Active.Total() == 0) Trade.EntryAtOpenBar(Rates[i+1], pos_type, 1.0); } uchar array[]; //-- Abfrage der Byte-Darstellung der geschlossenen Positionen Trade.ToCharArray(array); //-- Laden Sie die Byte-Darstellung in den Frame und weiterleiten zur weiteren Verarbeitung. FrameAdd(MTESTER_STR, MTESTER_ID, 0.0, array); return Trade.History.Total(); }
Sobald der Frame gebildet und zur Verarbeitung an die Methode OnTesterPass() geschickt wurde, ist es notwendig, herauszufinden, was mit dem Frame als Nächstes zu tun ist. Wie bereits erwähnt, besteht dieser Strategie-Tester aus zwei Teilen: Datengenerierungsblock und zusammengestellter Block für die Informationsanalyse. Bei einer solchen Analyse müssen alle generierten Frames in einem komfortablen und sparsames Format gespeichert werden, so dass dieses Format auch später problemlos analysiert werden kann. Dies geschieht mit Hilfe eines Zip-Archivs. Erstens werden die Daten effektiv komprimiert, was bedeutet, dass selbst Informationen über tausend Geschäfte nicht viel Platz beanspruchen würden. Zweitens bietet es ein komfortables Dateisystem. Jeder Durchlauf kann als separate Datei in einem einzigen Zip-Archiv gespeichert werden.
Also, lassen Sie uns eine Methode erstellen, um den Byteinhalt eines Frames in eine Zip-Archiv zu konvertieren:
//+------------------------------------------------------------------+ //| Ein neuer Durchlauf kommt ins Zip-Archiv | //+------------------------------------------------------------------+ void OnTesterPass() { ulong pass = 0; string name = ""; ulong id = 0; double value = 0.0; uchar data[]; FrameNext(pass, name, id, value, data); //-- Erstellen der Datei des Durchlaufs und Integration in das Zip-Archiv hinzu printf("Received new frame of size " + (string)ArraySize(data)); string file_name = name + "_" + (string)id + "_" + (string)pass + "_" + DoubleToString(value, 5)+".hps"; CZipFile* zip_file = new CZipFile(file_name, data); Zip.AddFile(zip_file); }
Da die Klasse für das Arbeiten mit ZIP-Archiven ziemlich mächtig ist und universelle Methoden hat, ist das Hinzufügen eines neuen Durchlaufs als separate Datei zum Archiv sehr einfach. Tatsächlich fügt OnTesterPass dem Zip-Archiv eine neue Zip-Datei hinzu, die auf globaler Ebene deklariert wurde:
CZip Zip; // Zip-Archiv zum Füllen während der Optimierungsdurchläufe
Dieses Verfahren wird am Ende jedes Optimierungslaufs parallel aufgerufen und ist nicht ressourcenintensiv.
Am Ende der Optimierung sollte das erzeugte Zip-Archiv einfach als entsprechende Zip-Datei gespeichert werden. Das ist auch sehr einfach. Dies geschieht in der Methode OnTesterDeinit():
//+------------------------------------------------------------------+ //| Speichern des Zip-Archivs aller Durchläufe | //+------------------------------------------------------------------+ void OnTesterDeinit() { Zip.SaveZipToFile(OptimizationFile, FILE_COMMON); string f_totals = (string)Zip.TotalElements(); printf("Optimization complete. Total optimization passes saved: " + f_totals); }
Hier ist OptimizationFile eine benutzerdefinierte Zeichenkette, der den Optimierungsnamen festlegt. Standardmäßig ist es "Optimization.zip". So wird nach Abschluss der Optimierung der aktualisierten SmaSample-Strategie ein entsprechendes Zip-Archiv erstellt. Es befindet sich im Ordner "Files" und wird standardmäßig geöffnet:
Abb. 7. Interner Inhalt der Optimierungsdatei
Wie Sie sehen, sind alle gespeicherten Durchläufe perfekt gespeichert und weisen eine hohe Kompressionsrate von 3 bis 5 auf.
Nachdem diese Daten gesammelt und auf der Festplatte gespeichert wurden, ist es notwendig, sie in ein anderes Programm zu laden und zu analysieren. Dies wird im nächsten Abschnitt behandelt.
Strategy Analyzer anlegen
Im vorigen Abschnitt wurde ein Zip-Archiv erstellt, das die Informationen von allen Durchläufen enthält. Diese Information sollten nun verarbeitet werden. Zu diesem Zweck erstellen wir ein spezielles Programm namens M-Tester Analyzer. Es lädt das erzeugte Archiv und zeigt jeden Durchlauf als komfortables Diagramm des Saldos an. Der M-Tester Analyzer berechnet auch zusammenfassende Statistiken für den ausgewählten Durchgang.
Eines der Hauptmerkmale des gesamten Testkomplexes ist die Fähigkeit, Informationen über alle Durchgänge gleichzeitig zu speichern. Das bedeutet, dass es ausreicht, die Optimierung nur einmal durchzuführen. Alle seine Durchläufe werden in einem einzigen Archiv gespeichert und dem Benutzer zur Verfügung gestellt. Jeder Durchgang kann später aus dieser Optimierung geladen werden und seine Statistiken können eingesehen werden, ohne dass der Strategietester erneut gestartet werden muss.
Reihenfolge der Aktionen des Analysators:
- Laden des ausgewählten Optimierungsarchivs
- Wählen Sie einen der Optimierungsdurchläufe in diesem Archiv aus.
- Zeichnen Sie ein dynamisches Diagramm des virtuellen Gleichgewichts auf der Grundlage bestehender Positionen
- Berechnen Sie die grundlegenden Statistiken des Durchlaufs, einschließlich solcher Parameter wie Anzahl der Positionen, Gesamtgewinn, Gesamtverlust, Profit Factor, Erwartungswert, etc.
- Die berechnete Statistik wird im Hauptfenster des Programms tabellarisch ausgegeben.
Es ist notwendig, den Benutzer mit Mitteln auszustatten, um einen beliebigen Durchlauf aus dem Archiv auszuwählen: Erstellen wir einen einfachen Übergang vom aktuellen Durchlauf zum nächsten oder vorherigen, sowie die Möglichkeit, eine benutzerdefinierte Nummer eines Durchlaufs festzulegen.
Das Programm basiert auf der Grafik-Klasse CPanel. Derzeit gibt es keinen Artikel, der dieser Bibliothek gewidmet ist, jedoch ist sie leicht zu erlernen, kompakt und wurde wiederholt in verschiedenen Projekten und Artikeln verwendet.
Der Hauptcode des Analysators befindet sich in der Klasse CAnalizePanel, abgeleitet von CElChart. Der Analysator selbst wird in Form eines Experten implementiert. Die Hauptdatei des Experten startet das grafische Fenster des Analysators. Hier ist es:
//+------------------------------------------------------------------+ //| mAnalizer.mq5 | //| Copyright 2017, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #include "mAnalizerPanel.mqh" CAnalyzePanel Panel; input string FilePasses = "Optimization.zip"; //+------------------------------------------------------------------+ //| Initialisierungsfunktion des Experten | //+------------------------------------------------------------------+ int OnInit() { //--- Panel.Width(800); Panel.Height(630); Panel.XCoord(10); Panel.YCoord(20); Panel.LoadFilePasses(FilePasses); Panel.Show(); ChartRedraw(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Deinitialisierungsfunktion des Experten | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { Panel.Hide(); } //+------------------------------------------------------------------+ //| Funktion eines Chart-Events | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { switch(id) { case CHARTEVENT_OBJECT_ENDEDIT: { CEventChartEndEdit event(sparam); Panel.Event(&event); break; } case CHARTEVENT_OBJECT_CLICK: { CEventChartObjClick event(sparam); Panel.Event(&event); break; } } ChartRedraw(); } //+------------------------------------------------------------------+
Wie Sie sehen können, ist der Code extrem einfach. Es wird ein Objekt vom Typ CAnalyzePanel angelegt. Dann werden seine Größen in der OnInit-Methode eingestellt, danach wird er auf dem aktuellen Chart angezeigt (die Show-Methode). Von allen Ereignissen, die aus dem Chart kommen, sind nur zwei von Interesse: das Ende der Texteingabe und der Klick auf das grafische Objekt. Diese Ereignisse werden in das spezielle Objekt CEvent umgewandelt und an das Panel übergeben (Panel.Event(...)). Das Analysator-Panel empfängt diese Ereignisse und verarbeitet sie.
Lassen Sie uns nun das Analysator-Panel selbst beschreiben. Es besteht aus einer großen Klasse CAnalyzePanel, so dass nicht als Ganzes dargestellt wird. Ihr vollständiger Code ist am Ende des Artikels für Interessierte beigefügt. Es wird hier nur eine kurze Beschreibung der Funktionsweise mit Hilfe des folgenden Prototypen der Klasse gegeben:
//+------------------------------------------------------------------+ //| mAnalizer.mq5 | //| Copyright 2017, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #include <Panel\ElChart.mqh> #include <Panel\ElButton.mqh> #include <Graphics\Graphic.mqh> #include "ListPass.mqh" #include "TradeAnalyze.mqh" //+------------------------------------------------------------------+ //| Panel zur Analyse der Durchläufe des mathematischen Analysators | //+------------------------------------------------------------------+ class CAnalizePanel : public CElChart { private: //-- Arrays der Elemente mit ihrer Statistik CArrayObj m_stat_descr; // Beschreibung der Statistik CArrayObj m_stat_all; // Wert der Statistik aller Positionen CArrayObj m_stat_long; // Wert der Statistik aller Kaufpositionen CArrayObj m_stat_short; // Wert der Statistik aller Verkaufspositionen CTradeAnalize m_analize; // Statistisches Berechnungsmodul //-- Grafisches Steuerelement CElChart m_name_analyze; // Name des Hauptfensters CElChart m_np; // "Durchlauf #" Text CElChart m_of_pass; // "von ### Durchläufen" Text CElChart m_pass_index; // Durchlaufnummer des Eingabefelds CElButton m_btn_next; // Taste für den "Nächster Durchlauf" CElButton m_btn_prev; // Taste für den "Vorherigen Durchlauf" CGraphic m_graphic; // Diagramm der Saldenkurve //-- Infrastruktur CListPass m_passes; // Liste der Durchläufe int m_curr_pass; // Index des aktuellen Durchlaufs CCurve* m_balance_hist; // Saldenkurve auf dem Chart bool IsEndEditPass(CEvent* event); bool IsClick(CEvent* event, CElChart* el); void NextPass(void); void PrevPass(void); int GetCorrectPass(string text); void RedrawGraphic(void); void RedrawCurrPass(void); void PlotStatistic(void); string TypeStatToString(ENUM_MSTAT_TYPE type); void CreateStatElements(void); string ValueToString(double value, ENUM_MSTAT_TYPE type); public: CAnalizePanel(void); bool LoadFilePasses(string file_name, int file_common = FILE_COMMON); virtual void OnShow(); virtual void OnHide(); virtual void Event(CEvent *event); };
Wie Sie sehen können, ist das Hauptwerk dieser Klasse im Inneren versteckt. Unter den 'public' Methoden ist die Hauptmethode das Laden der Zip-Datei, die die Optimierungsdurchläufe enthält. Die gesamte Arbeit der Klasse kann in drei Teile gegliedert werden:
- Erstellen eines Diagramms und Hinzufügen einer Saldenkurve zu diesem Diagramm.
- Erstellen von Text-Labels in Form von CElChart Controls, die die Teststatistik anzeigen.
- Aktuelle Berechnung der Statistik der Durchläufe.
Lassen Sie uns jeden dieser Abschnitte kurz beschreiben.
Es ist notwendig, eine ganze Reihe von Kontrollen zu erstellen, um alle gesammelten Statistiken für jeden Durchgang anzuzeigen. Der Analysator zeigt zehn grundlegende statistische Parameter an. Zusätzlich wird jeder Parameter separat für alle Kauf- und Verkaufspositionen berechnet. 10 zusätzliche Kennzeichnungen sind erforderlich, um die Bezeichnungen der Indikatoren anzuzeigen. Daher ist es notwendig, 40 Benennungen zu erstellen. Um zu vermeiden, dass jede Steuerung manuell erstellt werden muss, machen wir das automatisch. Dazu wird jedem berechneten, statistischen Parameter in einer speziellen Aufzählung eine eigene Enumeration zugewiesen:
//+------------------------------------------------------------------+ //| Identifikatoren des Statistikwerttyps | //+------------------------------------------------------------------+ enum ENUM_MSTAT_TYPE { MSTAT_PROFIT, MSTAT_ALL_WINS_MONEY, MSTAT_ALL_LOSS_MONEY, MSTAT_TRADERS_TOTAL, MSTAT_WIN_TRADERS, MSTAT_LOSS_TRADERS, MSTAT_MAX_PROFIT, MSTAT_MAX_LOSS, MSTAT_PROFIT_FACTOR, MSTAT_MATH_EXP, }; #define MSTAT_ELEMENTS_TOTAL 10
Wir de3finieren auch einen Bezeichner für die Handelsrichtung:
//+------------------------------------------------------------------+ //| Die Statistik kann für eine von drei Richtungen berechnet werden | //+------------------------------------------------------------------+ enum ENUM_MSTATE_DIRECT { MSTATE_DIRECT_ALL, // Für alle Positionen MSTATE_DIRECT_LONG, // Für Kaufpositionen MSTATE_DIRECT_SHORT, // Für Verkaufspositionen };
Das Panel enthält vier Gruppen von Steuerelementen, die sich jeweils in einem eigenen Array befinden:
- Steuerelemente zur Anzeige der Statistiknamen (das Array m_stat_descr)
- Steuerelemente, die die Werte der Statistik für alle Positionen anzeigen (das Array m_stat_all)
- Steuerelemente, die die Werte der Statistik für Kaufpositionen anzeigen (das Array m_stat_long)
- Steuerelemente, die die Werte der Statistik für Verkaufspositionen anzeigen (das Array m_stat_short)
Alle diese Steuerelemente werden beim ersten Start in der Methode CAnalyzePanel::CreateStatElements(void) erzeugt.
Sobald alle Steuerelemente erstellt sind, sollten sie mit den richtigen Werten gefüllt werden. Die Berechnung dieser Werte wird an die externe KLasse CTradeAnalize delegiert:
#include <Arrays\ArrayObj.mqh> #include <Dictionary.mqh> #include "..\MTester\Mposition.mqh" //+------------------------------------------------------------------+ //| Hilfssteuerung mit den notwendigen Feldern | //+------------------------------------------------------------------+ class CDiffValues : public CObject { public: double all; double sell; double buy; CDiffValues(void) : all(0), buy(0), sell(0) { } }; //+------------------------------------------------------------------+ //| Klasse der stat. Analyse | //+------------------------------------------------------------------+ class CTradeAnalize { private: CDictionary m_values; public: void CalculateValues(CArrayObj* history); double GetStatistic(ENUM_MSTAT_TYPE type, ENUM_MSTATE_DIRECT direct); }; //+------------------------------------------------------------------+ //| Berechnen der stat. Werte | //+------------------------------------------------------------------+ double CTradeAnalize::GetStatistic(ENUM_MSTAT_TYPE type, ENUM_MSTATE_DIRECT direct) { CDiffValues* value = m_values.GetObjectByKey(type); switch(direct) { case MSTATE_DIRECT_ALL: return value.all; case MSTATE_DIRECT_LONG: return value.buy; case MSTATE_DIRECT_SHORT: return value.sell; } return EMPTY_VALUE; } //+------------------------------------------------------------------+ //| Berechnen der Anzahl der Positionen für jede Richtung | //+------------------------------------------------------------------+ void CTradeAnalize::CalculateValues(CArrayObj *history) { m_values.Clear(); for(int i = 0; i < MSTAT_ELEMENTS_TOTAL; i++) m_values.AddObject(i, new CDiffValues()); CDiffValues* profit = m_values.GetObjectByKey(MSTAT_PROFIT); CDiffValues* wins_money = m_values.GetObjectByKey(MSTAT_ALL_WINS_MONEY); CDiffValues* loss_money = m_values.GetObjectByKey(MSTAT_ALL_LOSS_MONEY); CDiffValues* total_traders = m_values.GetObjectByKey(MSTAT_TRADERS_TOTAL); CDiffValues* win_traders = m_values.GetObjectByKey(MSTAT_WIN_TRADERS); CDiffValues* loss_traders = m_values.GetObjectByKey(MSTAT_LOSS_TRADERS); CDiffValues* max_profit = m_values.GetObjectByKey(MSTAT_MAX_PROFIT); CDiffValues* max_loss = m_values.GetObjectByKey(MSTAT_MAX_LOSS); CDiffValues* pf = m_values.GetObjectByKey(MSTAT_PROFIT_FACTOR); CDiffValues* mexp = m_values.GetObjectByKey(MSTAT_MATH_EXP); total_traders.all = history.Total(); for(int i = 0; i < history.Total(); i++) { CMposition* pos = history.At(i); profit.all += pos.Profit(); if(pos.PositionType() == POSITION_TYPE_BUY) { if(pos.Profit() > 0) { win_traders.buy++; wins_money.buy += pos.Profit(); } else { loss_traders.buy++; loss_money.buy += pos.Profit(); } total_traders.buy++; profit.buy += pos.Profit(); } else { if(pos.Profit() > 0) { win_traders.sell++; wins_money.sell += pos.Profit(); } else { loss_traders.sell++; loss_money.sell += pos.Profit(); } total_traders.sell++; profit.sell += pos.Profit(); } if(pos.Profit() > 0) { win_traders.all++; wins_money.all += pos.Profit(); } else { loss_traders.all++; loss_money.all += pos.Profit(); } if(pos.Profit() > 0 && max_profit.all < pos.Profit()) max_profit.all = pos.Profit(); if(pos.Profit() < 0 && max_loss.all > pos.Profit()) max_loss.all = pos.Profit(); } mexp.all = profit.all/total_traders.all; mexp.buy = profit.buy/total_traders.buy; mexp.sell = profit.sell/total_traders.sell; pf.all = wins_money.all/loss_money.all; pf.buy = wins_money.buy/loss_money.buy; pf.sell = wins_money.sell/loss_money.sell; }
Die Berechnung selbst wird mit der Methode CalculateValues durchgeführt. Es sollte ein CArrayObj-Array übergeben werden, das CMposition-Steuerelemente enthält. Aber woher kommt dieses Array virtueller Positionen?
Tatsache ist, dass die Klasse CAnalyzePanel eine weitere Klasse enthält — CListPass. Sie ist diejenige, die das Zip-Archiv lädt und eine Sammlung von Durchläufen erstellt. Diese Klasse ist sehr einfach:
//+------------------------------------------------------------------+ //| Optimazer.mq5 | //| Copyright 2017, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #include <Zip\Zip.mqh> #include <Dictionary.mqh> #include "..\MTester\MTrade.mqh" //+------------------------------------------------------------------+ //| Speichert die Liste der Optimierungsdurchläufe | //+------------------------------------------------------------------+ class CListPass { private: CZip m_zip_passes; // Archive aller Optimierungsdurchläufe CDictionary m_passes; // Geladene, geschlossene Positionen public: bool LoadOptimazeFile(string file_name, int file_common = FILE_COMMON); int PassTotal(void); CArrayObj* PassAt(int index); }; //+------------------------------------------------------------------+ //| Lädt die Liste der Optimierungsdurchläufe aus einem Zip-Archiv | //+------------------------------------------------------------------+ bool CListPass::LoadOptimazeFile(string file_name,int file_common=FILE_COMMON) { m_zip_passes.Clear(); if(!m_zip_passes.LoadZipFromFile(file_name, file_common)) { printf("Failed load optimization file. Last Error"); return false; } return true; } //+------------------------------------------------------------------+ //| Anzahl der Durchläufe | //+------------------------------------------------------------------+ int CListPass::PassTotal(void) { return m_zip_passes.TotalElements(); } //+------------------------------------------------------------------+ //| Rückgabe der Liste der Positionen der Durchläufe des Index | //+------------------------------------------------------------------+ CArrayObj* CListPass::PassAt(int index) { if(!m_passes.ContainsKey(index)) { CZipFile* zip_file = m_zip_passes.ElementAt(index); uchar array[]; zip_file.GetUnpackFile(array); CMtrade* trade = new CMtrade(); trade.FromCharArray(array); m_passes.AddObject(index, trade); } CMtrade* trade = m_passes.GetObjectByKey(index); //printf("Total Traders: " + (string)trade.History.Total()); return &trade.History; }
Wie man sieht, lädt die Klasse CListPass das Optimierungsarchiv, entpackt es aber nicht. Das bedeutet, dass alle Daten in komprimierter Form auch im Arbeitsspeicher des Computers gespeichert werden, wodurch weniger Arbeitsspeicher des Computers benötigt wird. Der angeforderte Durchlauf wird entpackt und in ein Objekt CMtrade konvertiert, wonach er in entpackter Form im internen Speicher abgelegt wird. Beim nächsten Aufruf dieses Steuerelements entfällt das Auspacken.
Siehe hierzu auch die Klasse CAnalyzePanel. Wir wissen jetzt, woher die Positionen geladen werden (die Klasse CListPass) und wie ihre Statistiken berechnet werden (die Klasse CTradeAnalyze). Nachdem die grafischen Steuerelemente erstellt sind, müssen diese noch mit den richtigen Werten ausgefüllt werden. Dies geschieht mit der Methode CAnalyzePanel::PlotStatistic(void):
//+------------------------------------------------------------------+ //| Anzeige der Statistik | //+------------------------------------------------------------------+ void CAnalyzePanel::PlotStatistic(void) { if(m_stat_descr.Total() == 0) CreateStatElements(); CArrayObj* history = m_passes.PassAt(m_curr_pass-1); m_analize.CalculateValues(history); for(int i = 0; i < MSTAT_ELEMENTS_TOTAL; i++) { ENUM_MSTAT_TYPE stat_type = (ENUM_MSTAT_TYPE)i; //-- alle Positionen CElChart* el = m_stat_all.At(i); string v = ValueToString(m_analize.GetStatistic(stat_type, MSTATE_DIRECT_ALL), stat_type); el.Text(v); //-- Kaufpositionen el = m_stat_long.At(i); v = ValueToString(m_analize.GetStatistic(stat_type, MSTATE_DIRECT_LONG), stat_type); el.Text(v); //-- Verkaufspositionen el = m_stat_short.At(i); v = ValueToString(m_analize.GetStatistic(stat_type, MSTATE_DIRECT_SHORT), stat_type); el.Text(v); } }
Alle grundlegenden Steuerelemente, die für den Betrieb des Analysepanels erforderlich sind, wurden berücksichtigt. Die Beschreibung erwies sich als widersprüchlich, aber das ist das Wesen der Programmierung: Alle Elemente sind miteinander verbunden, und manchmal müssen alle gleichzeitig beschrieben werden.
Nun ist es an der Zeit, den Analysator auf dem Chart zu starten. Stellen Sie zuvor sicher, dass das Zip-Archiv mit den Optimierungsergebnissen im Verzeichnis FILE_COMMON vorhanden ist. Standardmäßig lädt der Analysator die Datei "Optimization.zip", die sich im gemeinsamen Verzeichnis befinden muss.
Die größte Wirkung der implementierten Eigenschaft zeigt sich beim Umschalten der Durchläufe. Das Diagramm und die Statistiken werden automatisch aktualisiert. Der Screenshot unten zeigt diesen Moment:
Abb. 8. Wechsel der Durchläufe im Analysator für die "Mathematische Berechnung"
Zum besseren Verständnis der Bedienung des Panels ist die Darstellung mit Tooltips dargestellt: Die wichtigsten Steuerelemente sind mit Rahmen versehen, die die Klasse und die Methoden angeben, die für jede Gruppe von Steuerelementen verantwortlich sind:
Abb. 9. Hauptelemente der Schnittstelle
Lassen Sie uns abschließend die Struktur des resultierenden Projektes beschreiben. Alle Quellcodes befinden sich im Archiv MTester.zip. Das Projekt selbst befindet sich im Ordner"MQL5\Experts\MTester". Allerdings erfordert das Projekt, wie jedes andere komplexe Programm, die Einbindung zusätzlicher Bibliotheken. Diejenigen, die im Standardpaket MetaTrader 5 nicht verfügbar sind, befinden sich in diesem Archiv im Ordner MQL5\Include. Zuerst einmal ist es die Grafikbibliothek CPanel (Ort: MQL5\Include\Panel). Außerdem sind die Bibliothek für die Arbeit mit Zip-Archiven (MQL5\Include\Zip) und die Klasse für die Organisation eines assoziativen Arrays enthalten (MQL5\Include\Dictionary). Für die Bequemlichkeit der Benutzer wurden zwei MQL5-Projekte erstellt. Dies ist eine neue Funktion von MetaTrader 5, die vor kurzem implementiert wurde. Das erste Projekt heißt MTester und beinhaltet den Strategietester und die Strategie selbst, die auf den Kreuzungspunkten der gleitenden Durchschnitte (SmaSample.mq5) basiert. Das zweite Projekt heißt MAnalyzer und enthält den Quellcode des Analysator-Panels.
Neben den Quellcodes enthält das Archiv die Datei Optimization.zip mit den Optimierungsergebnissen, mit etwa 160 Durchläufen der Strategie auf den Testdaten. Dies ermöglicht eine schnelle Überprüfung der Funktionalität des Analysators der Durchläufe, ohne dass eine erneute Optimierung durchgeführt werden muss. Die Datei befindet sich in MQL5\Files.
Schlussfolgerung
Abschließend sind hier die kurzen Thesen der im Artikel beschriebenen Inhalte.
- Der mathematische Tester hat eine hohe Geschwindigkeit, da die Simulation der Handelsumgebung fehlt. Dies macht ihn zu einer guten Basis, um einen benutzerdefinierten Hochleistungsalgorithmus zum Testen einfacher Handelsstrategien zu erstellen. Da jedoch keine Kontrolle über die Richtigkeit der ausgeführten Handelsoperationen besteht, ist es möglich, versehentlich "in die Zukunft zu blicken" — verwenden noch nicht eingetroffene Kurse. Fehler in solchen "Grails" sind schwer zu identifizieren, aber das ist der Preis für hohe Leistung.
- Es ist nicht möglich, die Handelsumgebung aus dem Mathematik-Berechnungs-Tester heraus aufzurufen. Folglich ist es unmöglich, die Kurse des gewünschten Instruments abzurufen. Daher ist es in diesem Modus notwendig, die erforderlichen Daten vorab manuell herunterzuladen und eigene Bibliotheken für die Berechnung der Indikatoren zu verwenden. Der Artikel zeigte, wie man die Daten aufbereitet, wie man sie effizient komprimiert und wie man sie in das Programmausführungsmodul integriert. Diese Technik kann auch für all diejenigen nützlich sein, die mit dem Programm zusätzliche Daten verteilen wollen, die für den Betrieb des Programms benötigt werden.
- Im Modus der "Mathematischen Berechnung" gibt es auch keinen Zugriff auf Standardindikatoren. Daher ist es notwendig, die notwendigen Indikatoren manuell zu berechnen. Aber auch die Geschwindigkeit ist sehr wichtig. Damit ist die manuelle Berechnung der Indikatoren innerhalb des Experten nicht nur die einzige, sondern auch die schnellste Lösung. Glücklicherweise ist die Ringpufferbibliothek in der Lage, eine effiziente Berechnung aller notwendigen Indikatoren in konstanter Zeit zu ermöglichen.
- Das Erstellen von Frames im MetaTrader 5 ist ein mächtiger, wenn auch komplizierter Mechanismus, der dem Benutzer große Möglichkeiten beim Schreiben von benutzerdefinierten Analysealgorithmen bietet. Zum Beispiel ist es möglich, einen benutzerdefinierten Strategie-Tester zu erstellen, der in diesem Artikel gezeigt wurde. Um das volle Potenzial der Erstellung von Frames auszuschöpfen, müssen Sie in der Lage sein, mit binären Daten in vollem Umfang zu arbeiten. Durch die Fähigkeit, mit diesem Datentyp zu arbeiten, wird es möglich, komplexe Daten zu erzeugen, z.B. eine Liste von Planstellen. Der Artikel zeigte: wie man einen komplexen benutzerdefinierten Datentyp (die CMPosition-Klasse von Positionen) erstellt; wie man ihn in eine Byte-Darstellung konvertiert und dem Frame hinzufügt; wie man ein Array von Bytes aus dem Frame holt und sie zurück in die benutzerdefinierte Liste von Positionen konvertiert.
- Einer der wichtigsten Bestandteile des Strategietesters ist das Datenspeichersystem. Offensichtlich ist die Menge der Daten, die während des Tests gewonnen werden, enorm: Jede Prüfung kann Hunderte oder sogar Tausende von Durchläufen umfassen und jeder Durchlauf beinhaltet viele Positionen, die leicht bis zu mehreren zehntausend erreichen können. Der Erfolg des gesamten Projekts hängt davon ab, wie effektiv diese Informationen gespeichert und verteilt werden. Daher wurde ein Zip-Archiv ausgewählt. Aufgrund der Tatsache, dass MQL5 über eine leistungsfähige und schnelle Bibliothek für die Arbeit mit dieser Art von Dateien verfügt, wird es einfach, eine benutzerdefinierte Dateiablage für Optimierungsläufe zu organisieren. Jede Optimierung führt zu einer einzelnen Zip-Datei, die alle Durchläufe enthält. Jeder Durchlauf wird durch eine einzige komprimierte Datei repräsentiert. Die Archivierung führt zu einer hohen Datenkomprimierung, die selbst bei einer groß angelegten Optimierung eine bescheidene Größe hat.
- Das Erstellen eines benutzerdefinierten Strategie-Testers reicht nicht aus. Für die Analyse dieser Optimierungsergebnisse ist ein eigenes Subsystem erforderlich. Ein solches Subsystem ist in Form des Programms M-Tester Analyzer implementiert. Hierbei handelt es sich um ein selbstständiges Programmmodul, das die Optimierungsergebnisse als Zip-Archiv lädt und als Diagramm anzeigt, in dem die Basisstatistiken für jeden Durchlauf dargestellt werden. Der M-Tester Analyzer basiert auf mehreren Klassen und der Grafikbibliothek CPanel. Es ist eine einfache und bequeme Bibliothek, mit der man schnell eine leistungsfähige grafische Oberfläche erstellen kann. Mit Hilfe der Systembibliothek CGraphic zeigt der Analysator ein aussagekräftiges Diagramm der Saldenkurve an.
Trotz der Tatsache, dass ein beeindruckendes Ergebnis erzielt wurde, und der Tester, der auf "Mathematischen Berechnung" des aktuell erzielten Gewinns basiert, fehlen ihm noch viele notwendige Dinge. Hier sind einige der prioritären Komponenten, die in den nächsten Versionen hinzugefügt werden müssen.
- Informationen zum Symbol (Name, Tick-Wert, Symbol, Spread, etc.). Diese Informationen sind notwendig, um die möglichen Provisionen, Spreads und Swaps zu berechnen. Sie wird auch für die Berechnung des Gewinns in der Depotwährung benötigt (derzeit wird der Gewinn in Punkten berechnet).
- Informationen über die Strategie und ihre Parameter für jeden Durchgang. Es ist notwendig, nicht nur das Ergebnis der Strategie, sondern auch alle Parameterwerte zu kennen. Dazu sollte der generierte Bericht auch einen zusätzlichen Datentyp enthalten.
- Kontrolle der Korrektheit der durchgeführten Aktionen. An dieser Stelle ist es ganz einfach, "in die Zukunft zu blicken", was zu einem "Heiligen Gral" führt, der aber nichts mit der Realität zu tun hat. In den zukünftigen Versionen ist zumindest ein minimaler Steuerungsmechanismus erforderlich. Allerdings ist es nach wie vor schwierig zu bestimmen, wie es aussehen soll.
- Integration der Erstellung der berichte in den realen Strategie-Tester. Nichts hindert uns daran, das Ergebnis des standardmäßigen MetaTrader 5 Strategie-Testers in das entwickelte Reportformat umzuwandeln. Dadurch ist es möglich, zuverlässige Testergebnisse mit dem M-Trade Analyzer zu analysieren. So wird es mehrere Prüfsysteme und ein Analysesystem geben.
- Weiterentwicklung des M-Trade Analyzers. Das Programm verfügt derzeit nur über die grundlegenden Funktionen. Diese reichen eindeutig nicht aus, um die Daten vollständig zu verarbeiten. Es ist notwendig, zusätzliche Statistiken und separate Saldenkurven sowohl für die Kauf- und die Verkaufspositionen hinzuzufügen. Es wäre auch sinnvoll, zu lernen, wie man die geschlossenen Positionen in einer Textdatei speichert, um sie dann z.B. in Excel zu laden.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/4226
![LifeHack für Händler: Fast-Food aus Indikatoren](https://c.mql5.com/2/30/LifeHack_MQL4.png)
![Automatisches Erstellen von Unterstützung- und Widerstandslinien](https://c.mql5.com/2/30/Auto_support_resisitance.png)
![LifeHack für Händler: ForEach mit #define zubereiten](https://c.mql5.com/2/31/ForEachwdefine.png)
![Automatische Auswahl vielversprechender Signale](https://c.mql5.com/2/30/xf1zfo07t1b6ty_wozfke_cxp3ajzhsku9i_e6dfkszd.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.