Techniken des MQL5-Assistenten, die Sie kennen sollten (Teil 02): Kohonen-Karten
1. Einführung
1.1 In der Fortsetzung dieser Serie über den MQL5-Assistenten werden wir uns diesmal mit Kohonen-Maps (Kohnen-Karten) beschäftigen. Diese per Wikipedia werden verwendet, um eine niedrigdimensionale (typischerweise zweidimensionale) Darstellung eines höherdimensionalen Datensatzes zu erzeugen, wobei die topologische Struktur der Daten erhalten bleibt. Sie wurden in den 1980er Jahren von Teuvo Kohonen entwickelt.
Einfach ausgedrückt sind Kohonen-Maps (auch selbstorganisierende Karten genannt) in der Lage, Komplexität zusammenzufassen, ohne die Unterscheidbarkeit des Zusammengefassten zu verlieren. Die Zusammenfassung dient als eine Form der Organisation, daher der Name Selbstorganisation. Mit den neu organisierten Daten oder Karten haben wir also zwei zusammenhängende Datensätze. Die ursprünglichen hochdimensionalen Daten, die uns als Eingabe dienen, und die zusammengefasste Form (niedrigerdimensionale Daten), die normalerweise, aber nicht immer, in zwei Dimensionen dargestellt wird, als Ausgabe. Die Eingaben sind das Bekannte, während die Ausgaben das Unbekannte oder in diesem Fall das, was "untersucht" wird.
Für einen Händler, wenn wir uns in diesem Artikel nur auf zeitbasierte Preisreihen konzentrieren, sind die bekannten Daten (die wir als Feed-Daten bezeichnen) zu jeder Zeit die Preise links von dieser Zeit mit den unbekannten Daten (die wir Funktor-Daten nennen), die auf der rechten Seite liegen. Die Art und Weise, wie wir die bekannten und unbekannten Daten klassifizieren, bestimmt die jeweilige Anzahl der Dimensionen sowohl für die Feed- als auch für die Funktordaten. Dies ist etwas, das Händler kritisch betrachten sollten, da es in hohem Maße von ihren Ansichten und ihrer Herangehensweise an die Märkte abhängt.
1.2 Ein häufiges Missverständnis bei diesen Karten ist, dass die Funktordaten ein Bild oder zweidimensional sein sollten. Bilder wie das folgende werden oft als repräsentativ für die Kohonen-Karten angesehen.
Es ist zwar nicht falsch, aber ich möchte betonen, dass der Funktor eine einzige Dimension haben kann und vielleicht auch sollte (für Händler). Anstatt also unsere hochdimensionalen Daten auf eine 2D-Karte zu reduzieren, werden wir sie auf eine einzelne Linie abbilden. Kohonen-Karten sind per Definition dazu gedacht, die Dimensionalität zu reduzieren, daher möchte ich in diesem Artikel einen Schritt weiter gehen. Die Kohonen-Karte unterscheidet sich von normalen neuronalen Netzen sowohl durch die Anzahl der Schichten als auch durch den zugrunde liegenden Algorithmus. Es handelt sich um eine einschichtige (in der Regel lineare 2D-Gitter, wie bereits erwähnt) Menge von Neuronen anstelle mehrerer Schichten. Alle Neuronen dieser Schicht, die wir als Funktor bezeichnen, sind mit dem Feed verbunden, aber nicht mit sich selbst. Das bedeutet, dass die Neuronen nicht direkt von den Gewichten der anderen Neuronen beeinflusst werden, sondern nur in Bezug auf die Feed-Daten aktualisiert werden. Die Funktordatenschicht ist häufig eine "Landkarte", die sich bei jeder Trainingsiteration in Abhängigkeit von den eingespeisten Daten neu organisiert. So hat jedes Neuron nach dem Training eine gewichtsangepasste Dimension in der Funktorschicht, die es ermöglicht, den euklidischen Abstand zwischen zwei beliebigen Neuronen zu berechnen.
2. Erstellen der Klasse
2.1 Struktur der Klasse.
2.1.1 Die abstrakte Klasse Dimension ist die erste Klasse, die wir definieren werden. Dieser Code wäre aufgeräumter, wenn ich das meiste davon in einer separaten Datei gemacht hätte und einfach darauf verweisen würde, aber ich möchte das zusammen mit den Geld- und Trailing-Klassen im nächsten Artikel abdecken, sodass im Moment wie im vorherigen Artikel der gesamte Code in der Signaldatei sein wird. Die Dimensionen sind in diesem Netz immer wichtig, da sie den Output stark beeinflussen. Die Eingabedaten (die Inputs) sind in der Regel mehrdimensional. Die Daten des Funktors (die Ausgänge) haben eine Dimension im Gegensatz zu den typischen x und y. Aufgrund der Mehrdimensionalität sowohl der Feed- als auch der Funktordaten wäre ein idealer Datentyp ein Double-Array.
Um jedoch dem Trend der Erforschung der MQL5-Bibliothek zu folgen, werden wir stattdessen eine Array-Liste vom Typ Double verwenden. Die Feed-Daten sind Änderungen der Tiefstwerte abzüglich der Änderungen der Höchstwerte über einen Zeitraum von einem Balken, wie wir ihn im vorherigen Artikel verwendet hatten. In der Regel werden Inputs besser auf der Grundlage der Marktkenntnisse eines Händlers ausgewählt und sollten nicht von jedem auf einem Live- oder sogar Testkonto verwendet werden. Jeder Händler sollte diesen Code ändern, um seine eigenen Eingabedaten zu berücksichtigen. Die Daten des Funktors werden wie angegeben eindimensional sein. Da es sich jedoch auch um eine Liste handelt, kann sie angepasst werden, um weitere Dimensionen hinzuzufügen. Für unsere Zwecke werden wir uns jedoch auf die Veränderung zwischen dem Eröffnungs- und dem Schlusskurs des letzten Balkens konzentrieren. Auch hier können wir mit dem MQL5-Assistenten festlegen, was ein Balken ist, indem wir Ihren eigenen Zeitrahmen auswählen. Die Klasse Dimension wird von CArrayList aus der MQL5-Codebibliothek abgeleitet. Diese Klasse wird um zwei Funktionen erweitert, nämlich Get und Set. Wie der Name schon sagt, helfen sie beim Abrufen und Setzen von Werten in der Liste, sobald ein Index angegeben ist.
#include <Generic\ArrayList.mqh> #include <Generic\HashMap.mqh> #define SCALE 5 #define IN_WIDTH 2*SCALE #define OUT_LENGTH 1 #define IN_RADIUS 100.0 #define OUT_BUFFER 10000 // //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class Cdimension : public CArrayList<double> { public: Cdimension() {}; ~Cdimension() {}; virtual double Get(const int Index) { double _value=0.0; TryGetValue(Index,_value); return(_value); }; virtual void Set(const int Index,double Value) { Insert(Index,Value); }; };
2.1.2 Die Klasse Feed wird von der soeben erstellten Klasse Dimension abgeleitet. Hier werden keine besonderen Funktionen hinzugefügt. Nur der Konstruktor gibt die Kapazität der Liste an (analog zur Array-Größe) und die Standardgröße unserer Feed-Data ist 10.
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class Cfeed : public Cdimension { public: Cfeed() { Clear(); Capacity(IN_WIDTH); }; ~Cfeed() { }; };
2.1.3 Functor Die Klasse wird ähnlich wie die Feed-Klasse sein, mit dem einzigen Unterschied in der Größe. Wie bereits erwähnt, betrachten wir 1 und nicht die üblichen 2 Dimensionen für unsere Funktordaten, sodass die Mengengröße 1 beträgt.
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class Cfunctor : public Cdimension { public: Cfunctor() { Clear(); Capacity(OUT_LENGTH); }; ~Cfunctor() { }; };
2.1.4 Neuron Diese Klasse ist der Punkt, an dem unser Code interessant wird. Wir werden sie als Klasse deklarieren, die von einer Schnittstelle in der MQL5-Bibliothek erbt, die zwei nutzerdefinierte Datentypen annimmt. Ein Schlüssel und ein Wert. Die betreffende Vorlagenschnittstelle ist die HashMap. Und die nutzerdefinierten Daten, die wir verwenden werden, sind die beiden oben angegebenen Klassen. Nämlich die Klasse Feed als Schlüssel und die Klasse Functor als Wert. Wir haben auch keine Funktionen, sondern nur Zeiger auf die Feed-Klasse, die Functor-Klasse und eine "Key-Value"-Klasse desselben. Der Zweck dieser Klasse ist, wie der Name schon sagt, die Definition des Neurons. Das Neuron ist unsere Dateneinheit, da es sowohl Eingangsdaten (Feeddaten) als auch Ausgangsdaten (Funktordaten) enthält. Die Futterdaten eines Neurons werden mit bereits trainierten Neuronen abgeglichen, um zu projizieren, was der Funktor sein könnte. Auch bei den abgebildeten Neuronen werden die Funktordaten angepasst, wenn ein neues Neuron trainiert wird.
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class Cneuron : public CHashMap<Cfeed*,Cfunctor*> { public: double weight; Cfeed *fd; Cfunctor *fr; CKeyValuePair < Cfeed*, Cfunctor* > *ff; Cneuron() { weight=0.0; fd = new Cfeed(); fr = new Cfunctor(); ff = new CKeyValuePair<Cfeed*,Cfunctor*>(fd,fr); Add(ff); }; ~Cneuron() { ZeroMemory(weight); delete fd; delete fr; delete ff; }; };
2.1.5 Layer Diese abstrakte Klasse ist das, was als Nächstes folgt. Sie erbt von einer Listenvorlage der Neuronenklasse und hat ein Objekt, einen Neuronenzeiger. Da es sich um eine abstrakte Klasse handelt, soll dieser Neuronenzeiger von Klassen verwendet werden, die von dieser Klasse abgeleitet werden. Diese Klassen sind 2, nämlich die Eingabeschicht und die Ausgabeschicht. Streng genommen sollten Kohonen-Karten nicht als neuronale Netze eingestuft werden, da sie keine Feed-Forward-Verbindungen mit Gewichten und Backpropagation haben. Einige Befürworter sind jedoch der Meinung, dass es sich um einen anderen Typ handelt.
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class Clayer : public CArrayList<Cneuron*> { public: Cneuron *n; Clayer() { n = new Cneuron(); }; ~Clayer() { delete n; }; };
2.1.6 Input Layer Die Klasse wird von der abstrakten Klasse CLayer abgeleitet. Hier werden die aktuellen Werte der Dateneinspeisung gespeichert, wenn das Netz verwendet wird. Anstatt einer typischen Schicht (layer) mit mehreren Neuronen wird es ein einziges Neuron geben, das die aktuellsten Feed- und Funktordaten hat, daher wird seine Größe 1 sein.
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class Cinput_layer : public Clayer { public: static const int size; Cinput_layer() { Clear(); Capacity(Cinput_layer::size); for(int s=0; s<size; s++) { n = new Cneuron(); Add(n); } } ~Cinput_layer() {}; }; const int Cinput_layer::size=1;
2.1.7 Output Layer Die Klasse wird ebenfalls von der Klasse CLayer abgeleitet, aber sie dient als unsere Karte, da hier "trainierte" Neuronen gespeichert werden. Der Funktordatenanteil der Neuronen in dieser Schicht entspricht einem Bild oder einer Karte Ihres typischen SOM. Seine Größe beträgt anfangs 10000 und wird um den gleichen Betrag erhöht, wenn neue Neuronen trainiert werden.
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class Coutput_layer : public Clayer { public: int index; int size; Coutput_layer() { index=0; size=OUT_BUFFER; Clear(); Capacity(size); for(int s=0; s<size; s++) { n = new Cneuron(); Add(n); } }; ~Coutput_layer() { ZeroMemory(index); ZeroMemory(size); }; };
2.1.8 Network Die Klasse Cnetwork wird wie die Neuronenklasse ebenfalls von der HashMap-Vorlagenschnittstelle abgeleitet. Ihre Schlüssel- und Wertdatentypen sind die Klasse der Eingabeschicht und die Klasse der Ausgabeschicht. Sie verfügt über die meisten Funktionen (9), um nicht nur die Listengröße zu ermitteln, sondern auch die Neuronen in den jeweiligen Schichten abzurufen und zu aktualisieren.
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class Cnetwork : public CHashMap<Cinput_layer*,Coutput_layer*> { public: Cinput_layer *i; Coutput_layer *o; CKeyValuePair < Cinput_layer*, Coutput_layer* > *io; Cneuron *i_neuron; Cneuron *o_neuron; Cneuron *best_neuron; Cnetwork() { i = new Cinput_layer(); o = new Coutput_layer(); io = new CKeyValuePair<Cinput_layer*,Coutput_layer*>(i,o); Add(io); i_neuron = new Cneuron(); o_neuron = new Cneuron(); best_neuron = new Cneuron(); }; ~Cnetwork() { delete i; delete o; delete io; delete i_neuron; delete o_neuron; delete best_neuron; }; virtual int GetInputSize() { TryGetValue(i,o); return(i.size); }; virtual int GetOutputIndex() { TryGetValue(i,o); return(o.index); }; virtual void SetOutputIndex(const int Index) { TryGetValue(i,o); o.index=Index; TrySetValue(i,o); }; virtual int GetOutputSize() { TryGetValue(i,o); return(o.size); }; virtual void SetOutputSize(const int Size) { TryGetValue(i,o); o.size=Size; o.Capacity(Size); TrySetValue(i,o); }; virtual void GetInNeuron(const int NeuronIndex) { TryGetValue(i,o); i.TryGetValue(NeuronIndex,i_neuron); }; virtual void GetOutNeuron(const int NeuronIndex) { TryGetValue(i,o); o.TryGetValue(NeuronIndex,o_neuron); }; virtual void SetInNeuron(const int NeuronIndex) { i.TrySetValue(NeuronIndex,i_neuron); }; virtual void SetOutNeuron(const int NeuronIndex) { o.TrySetValue(NeuronIndex,o_neuron); }; };
2.1.9 Map ist die endgültige Dachklasse. Es ruft eine Instanz der Netzklasse auf und enthält weitere Variablen für das Training der Neuronen und die Ermittlung des am besten passenden Neurons für das Netz.
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class Cmap { public: Cnetwork *network; static const double radius; static double time; double QE; //proxy for Quantization Error double TE; //proxy for Topological Error datetime refreshed; bool initialised; Cmap() { network = new Cnetwork(); initialised=false; time=0.0; QE=0.50; TE=5000.0; refreshed=D'1970.01.05'; }; ~Cmap() { ZeroMemory(initialised); ZeroMemory(time); ZeroMemory(QE); ZeroMemory(TE); ZeroMemory(refreshed); }; }; const double Cmap::radius=IN_RADIUS; double Cmap::time=10000/fmax(1.0,log(IN_RADIUS));
2.2 Topologie.
2.2.1 Neuronales Training ist der competitive learning (Lernwettbewerb), bei dem die Gewichte der vorhandenen Neuronen in der Ausgabeschicht angepasst und ein neues Trainerneuron hinzugefügt wird. Die Geschwindigkeit, mit der diese Gewichte angepasst werden, und vor allem die Anzahl der Wiederholungen, die zur Anpassung dieser Gewichte erforderlich sind, sind sehr empfindliche Parameter, die die Effizienz des Netzes bestimmen. Bei jeder Iteration der Anpassung der Gewichte wird ein neuer kleinerer Radius berechnet. Ich bezeichne diesen Radius als den functor-error (nicht zu verwechseln mit dem SOM Topological-error), aber die meisten bezeichnen ihn als den Nachbarschaftsradius, gemessen durch den euklidischen Abstand. Ich wähle "Fehler", da dies ein Parameter ist, der für bessere Netzwerkergebnisse minimiert werden muss. Je mehr Iterationen man durchführt, desto kleiner wird der Functor-Fehler. Neben der Anzahl der Iterationen muss auch die Lernrate schrittweise von einer Zahl nahe Eins gegen Null reduziert werden.
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CSignalKM::NetworkTrain(Cmap &Map,Cneuron &TrainNeuron) { Map.TE=0.0; int _iteration=0; double _training_rate=m_training_rate; int _err=0; double _functor_error=0.0; while(_iteration<m_training_iterations) { double _current_radius=GetTrainingRadius(Map,_iteration); for(int i=0; i<=Map.network.GetOutputIndex(); i++) { Map.network.GetOutNeuron(i); double _error = EuclideanFunctor(TrainNeuron,Map.network.o_neuron); if(_error<_current_radius) { _functor_error+=(_error); _err++; double _remapped_radius = GetRemappedRadius(_error, _current_radius); SetWeights(TrainNeuron,Map.network.o_neuron,_remapped_radius,_training_rate); Map.network.SetOutNeuron(i); } } _iteration++; _training_rate=_training_rate*exp(-(double)_iteration/m_training_iterations); } int _size=Map.network.GetOutputSize(), _index=Map.network.GetOutputIndex(); Map.network.SetOutputIndex(_index+1); if(_index+1>=_size) { Map.network.SetOutputSize(_size+OUT_BUFFER); } Map.network.GetOutNeuron(_index+1); for(int w=0; w<IN_WIDTH; w++) { Map.network.o_neuron.fd.Set(w,TrainNeuron.fd.Get(w)); } for(int l=0; l<OUT_LENGTH; l++) { Map.network.o_neuron.fr.Set(l,TrainNeuron.fr.Get(l)); } Map.network.SetOutNeuron(_index+1); if(_err>0) { _functor_error/=_err; Map.TE=_functor_error*IN_RADIUS; } }
2.2.2 Der Topologischer Fehler ist ein Schlüsselattribut in Kohonen-Karten. Ich betrachte dies als ein Maß dafür, wie nahe die Ausgabeschicht ihrem langfristigen Ziel ist. Vergessen wir nicht, dass die Neuronen der Ausgabeschicht mit jedem Training an das wahre oder beabsichtigte Ergebnis angepasst werden. Die Antwort darauf ist, dass wir diesem Ziel näher kommen, wenn wir die Ausgabeschicht mehr bewahren. Für die Zwecke dieses Artikels werde ich den Funktor-Fehler als Stellvertreter für ihn verwenden.
2.3 Quantisierung
2.3.1 Neuronen-Zuordnung ist ein Prozess, bei dem es darum geht, die Gewichte des Funktors zu finden, die am besten zu einem Neuron passen, für das nur Feed-Daten vorhanden sind. Dazu wird das Neuron in der Ausgabeschicht gefunden, das den kürzesten euklidischen Abstand zu dem Neuron aufweist, für das keine Funktordaten bekannt sind. Wie beim Training bezeichne ich diesen Abstand als feed-error. Auch hier gilt: Je kleiner unser Wert, desto zuverlässiger sollte das Netz sein.
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CSignalKM::NetworkMapping(Cmap &Map,Cneuron *MapNeuron) { Map.QE=0.0; Map.network.best_neuron = new Cneuron(); int _random_neuron=rand()%Map.network.GetOutputIndex(); Map.network.GetInNeuron(0); Map.network.GetOutNeuron(_random_neuron); double _feed_error = EuclideanFeed(Map.network.i_neuron,Map.network.o_neuron); for(int i=0; i<Map.network.GetOutputIndex(); i++) { Map.network.GetOutNeuron(i); double _error = EuclideanFeed(Map.network.i_neuron,Map.network.o_neuron); if(_error < _feed_error) { for(int w=0; w<IN_WIDTH; w++) { Map.network.best_neuron.fd.Set(w,Map.network.o_neuron.fd.Get(w)); } for(int l=0; l<OUT_LENGTH; l++) { Map.network.best_neuron.fr.Set(l,Map.network.o_neuron.fr.Get(l)); } _feed_error = _error; } } Map.QE=_feed_error/IN_RADIUS; }
3. Assemblieren mit dem MQL5 Wizard
3.1 Der Assistent für das Assemblieren ist einfach zu bedienen. Die einzige Warnung, die ich hier habe, ist, dass man zuerst mit großen Zeitrahmen testen sollte, da die idealen 10.000 Trainingsiterationen pro Balken einige Zeit dauern, wenn man über einen längeren Zeitraum trainiert.
4. Testen im Strategietester
4.1 Die Standardeingaben für unsere Tests werden die Empfindlichkeit unseres Quantisierungsfehler-Proxys (QE) und des topologischen Fehler-Proxys (TE) untersuchen. Wir werden uns zwei Szenarien ansehen. Zunächst werden wir mit sehr konservativen Werten testen, wobei QE und TE bei 0,5 und 12,5 liegen; dann werden wir diese Eingaben bei 0,75 bzw. 25,0 testen .
Konservative Optionen;
Aggressive Optionen.
Die Eingaben sind nicht sehr zahlreich. Es gibt die Option "training read", die bestimmt, ob vor der Initialisierung eine Trainingsdatei gelesen werden soll oder nicht. Fehlt diese Datei, wird der Experte nicht validieren. Außerdem gibt es die Funktion "training write", die, wie der Name schon sagt, festlegt, ob eine Lerndatei geschrieben werden soll, sobald der Experte seine Initialisierung aufhebt. Das Training findet immer dann statt, wenn der Experte im Einsatz ist. Die Option, nur zu trainieren und nicht zu handeln, wird durch den Eingabeparameter 'training only' festgelegt. Die beiden anderen wichtigen Parameter für Kohonen-Karten sind die 'training rate‘ (auch als Lernrate bezeichnet) und die Trainingsiterationen. Je höher diese beiden Werte sind (die Trainingsrate ist auf 1,0 begrenzt), desto besser ist die zu erwartende Leistung, allerdings auf Kosten von Zeit und CPU-Ressourcen.
Der Experte wurde für den V-förmigen Zeitraum von 2018.10.01 bis 2021.06.01 auf EURJPY trainiert und vom Enddatum des Trainings bis zum heutigen Datum vorwärts getestet.
Die konservative Option kam zu diesem Ergebnisbericht:
Und diese Kapitalkurve.
Die aggressivere Option hatte jedoch diesen Ergebnisbericht:
Und diese Kapitalkurve:
Natürlich sind weitere Tests und Feinabstimmungen in Bezug auf Risiko und Positionsgröße erforderlich, aber für ein System, das über einen so kurzen Zeitraum trainiert wird, ist es vielversprechend. Ein Vergleich der beiden obigen Szenarien zeigt jedoch, dass die konservativere Option nicht ausreichend belohnt wird, da ihr Sharpe-Ratio-Wert von 0,43 fast halb so hoch ist wie der Wert von 0,85 bei mehr Positionen. Hier ist vor dem Einsatz eine genauere Untersuchung erforderlich, und wie immer sollten neben der Anpassung des Feeds und der Funktordaten an den eigenen Handelsstil auch Vorabtests mit den Real-Ticks-Daten Ihres Brokers über längere Zeiträume durchgeführt werden, bevor der Einsatz erfolgt.
5. SCHLUSSFOLGERUNG
5.1 Der Der MQL5-Assistent ist eindeutig ein sehr agiles Werkzeug, wenn es darum geht, Handelssysteme in einem engen Zeitfenster zusammenzustellen. In diesem Artikel haben wir die Möglichkeit von Kohonen-Karten untersucht, die mehrdimensionale Daten von Preiszeitreihen in eine einzige Dimension übertragen, die von -1,0 bis 1,0 reicht. Dieser Ansatz ist zwar nicht üblich, aber er entspricht dem Wesen der Kohonen-Karten, die Komplexität zu reduzieren und die Entscheidungsfindung zu erleichtern. Wir haben dies auch getan, indem wir weiteren Code aus der MQL-Bibliothek wie Array-Listen und Hash-Maps vorgestellt haben. Ich hoffe, es hat Ihnen gefallen. Danke fürs Lesen.
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/11154
- 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.