
Entwicklung eines Expertenberaters für mehrere Währungen (Teil 3): Überarbeitung der Architektur
Einführung
In den vorangegangenen Artikeln haben wir begonnen, einen Multiwährungs-EA zu entwickeln, der gleichzeitig mit verschiedenen Handelsstrategien arbeitet. Die im zweiten Artikel vorgestellte Lösung unterscheidet sich bereits erheblich von der im ersten Artikel vorgestellten. Dies zeigt, dass wir noch auf der Suche nach den besten Optionen sind.
Versuchen wir, das entwickelte System als Ganzes zu betrachten und von den kleinen Details der Implementierung zu abstrahieren, um zu verstehen, wie man es verbessern kann. Verfolgen wir dazu die wenn auch kurze, so doch spürbare Entwicklung des Systems.
Erste Betriebsart
Wir haben ein EA-Objekt (der Klasse CAdvisor oder ihrer Nachkommen) zugewiesen, das ein Aggregator von Handelsstrategieobjekten (Klasse CStrategy oder ihrer Abkömmlinge) ist. Zu Beginn der EA-Operation geschieht Folgendes in OnInit():
- EA-Objekt wird erstellt.
- Objekte von Handelsstrategien werden erstellt und dem EA in seinem Array für Handelsstrategien hinzugefügt.
In OnTick() geschieht Folgendes:
- Die Methode CAdvisor::Tick() wird für das EA-Objekt aufgerufen.
- Diese Methode durchläuft alle Strategien und ruft deren Methode CStrategy::Tick() auf.
- Die Strategien innerhalb von CStrategy::Tick() führen alle notwendigen Operationen zum Öffnen und Schließen von Marktpositionen aus.
Dies kann schematisch wie folgt dargestellt werden:
Abb. 1. Betriebsart des ersten Artikels
Der Vorteil dieses Modus war, dass es möglich war, den EA über eine Reihe relativ einfacher Operationen mit anderen Instanzen von Handelsstrategien arbeiten zu lassen, vorausgesetzt, man hatte den Quellcode eines EA, der einer bestimmten Handelsstrategie folgte.
Der größte Nachteil hat sich jedoch schnell herausgestellt: Bei der Kombination mehrerer Strategien müssen wir die Größe der Positionen, die von jeder Instanz der Strategie eröffnet werden, mehr oder weniger reduzieren. Dies kann dazu führen, dass einige oder sogar alle Strategiefälle vollständig vom Handel ausgeschlossen werden. Je mehr Strategieinstanzen wir in die parallele Arbeit einbeziehen oder je kleiner die anfängliche Einlage ist, desto wahrscheinlicher ist ein solches Ergebnis, da die Mindestgröße der offenen Marktpositionen festgelegt ist.
Außerdem kam es bei der Zusammenarbeit mehrerer Strategieinstanzen zu einer Situation, in der entgegengesetzte Positionen der gleichen Größe geöffnet wurden. Bezogen auf das Gesamtvolumen entspricht dies dem Fehlen offener Positionen, aber es wurden weiterhin Swaps auf gegenläufige offene Positionen getätigt.
Zweite Betriebsart
Um die Unzulänglichkeiten zu beseitigen, haben wir beschlossen, alle Operationen mit Marktpositionen an einen separaten Ort zu verlagern, sodass die Handelsstrategien keine Möglichkeit mehr haben, direkt Marktpositionen zu eröffnen. Dies erschwert zwar die Überarbeitung fertiger Strategien, ist aber kein großer Verlust, da der Hauptnachteil des ersten Modus dadurch ausgeglichen wird.
Zwei neue Entitäten tauchen in unserem Betriebsmodus auf: virtuelle Positionen ( Klasse CVirtualOrder ) und ein Empfänger von Handelsvolumina aus Strategien (Klasse CReceiver und ihre Nachkommen).
Zu Beginn der EA-Operation geschieht Folgendes in OnInit():
- Ein Empfängerobjekt wird erstellt.
- Ein EA-Objekt wird erstellt und der erstellte Empfänger wird an dieses übergeben.
- Objekte von Handelsstrategien werden erstellt und dem EA in seinem Array für Handelsstrategien hinzugefügt.
- Jede Strategie erstellt ein eigenes Array von virtuellen Positionsobjekten mit der erforderlichen Anzahl dieser Objekte.
In OnTick() geschieht Folgendes:
- Die Methode CAdvisor::Tick() wird für das EA-Objekt aufgerufen.
- Diese Methode durchläuft alle Strategien und ruft deren Methode CStrategy::Tick() auf.
- Die Strategien innerhalb von CStrategy::Tick() führen alle notwendigen Operationen zum Öffnen und Schließen virtueller Positionen aus. Wenn ein Ereignis eintritt, das mit einer Änderung in der Zusammensetzung der offenen virtuellen Positionen verbunden ist, merkt sich die Strategie diese Änderung, indem sie ein Flag setzt.
- Wenn mindestens eine Strategie ein Änderungs-Flag gesetzt hat, startet der Empfänger die Methode zur Anpassung der offenen Volumina von Marktpositionen. Wenn die Anpassung erfolgreich ist, wird das Änderungs-Flag für alle Strategien zurückgesetzt.
Dies kann schematisch wie folgt dargestellt werden:
Abb. 2. Betriebsart vom zweiten Artikel
In dieser Betriebsart werden wir nicht mehr mit der Tatsache konfrontiert, dass eine bestimmte Strategie in keiner Weise die Größe der offenen Marktpositionen beeinflusst. Im Gegenteil, selbst eine Instanz, die ein sehr kleines virtuelles Volumen eröffnet, kann zu jenem Tropfen werden, der das Gesamtvolumen der virtuellen Positionen von mehreren Strategieinstanzen über das minimal zulässige Volumen einer Marktposition hinaus überschreitet. In diesem Fall wird die tatsächliche Marktposition eröffnet.
Auf dem Weg dorthin erhielten wir weitere angenehme Änderungen, darunter mögliche Einsparungen bei Swaps, eine geringere Belastung des Depots, weniger beobachtete Drawdowns und eine verbesserte Bewertung der Handelsqualität (Sharpe Ratio, Profitfaktor).
Beim Testen des zweiten Modus ist mir Folgendes aufgefallen:
- Jede Strategie behandelt zunächst bereits offene virtuelle Positionen, um die ausgelösten StopLoss- und TakeProfit-Levels zu bestimmen. Wenn eines der Niveaus erreicht ist, wird eine solche virtuelle Position geschlossen. Daher wurde diese Handhabung sofort in die Methode der Klasse CVirtualOrder verlagert. Aber diese Lösung scheint immer noch eine unzureichende Verallgemeinerung zu sein.
- Wir haben die Zusammensetzung der Basisklassen erweitert, indem wir neue erforderliche Entitäten hinzugefügt haben. Wenn wir keine virtuellen Positionen behandeln wollen, können wir solche Basisklassen trotzdem verwenden, indem wir ihnen einfach „leere“ Objekte übergeben. Wir können zum Beispiel das Objekt der Klasse CReceiver erstellen, das nur einfache, leere Methodenstrukturen enthält. Aber auch dies scheint eher eine Übergangslösung zu sein, die überarbeitet werden muss.
- Wir haben der Basisklasse CStrategy neue Methoden und die Eigenschaft für die Verfolgung von Änderungen in der Zusammensetzung offener virtueller Positionen hinzugefügt, was sich auf die Verwendung dieser Methoden in der Basisklasse CAdvisor ausgewirkt hat. Auch hier sieht es so aus, als würden die Möglichkeiten eingeschränkt und eine zu spezifische Implementierung in der Basisklasse vorgeschrieben.
- Die Basisklasse CStrategy erhielt die Methode Volume(), die das Gesamtvolumen der offenen virtuellen Positionen zurückgibt, da die entwickelte Empfängerklasse CVolumeReceiver Daten über die offenen virtuellen Volumina der einzelnen Strategien benötigte. Damit haben wir jedoch die Möglichkeit abgeschnitten, virtuelle Positionen für mehrere Symbole innerhalb einer Instanz einer Handelsstrategie zu eröffnen. In diesem Fall verliert das Gesamtvolumen seine Bedeutung. Diese Lösung eignet sich zum Testen von Ein-Symbol-Strategien, mehr aber auch nicht.
- Wir haben das Array für die Speicherung der Zeiger auf EA-Strategien in der CReceiver-Klasse verwendet, damit der Empfänger sie verwenden kann, um das offene virtuelle Volumen der Strategien herauszufinden. Dies führte zu einer Verdoppelung des Codes, der die Strategie-Arrays im EA und im Empfänger füllt.
- Jede Strategie eröffnet Positionen eines einzelnen Symbols: Wenn Sie einen Empfänger zur Gruppe der Strategien hinzufügen, wird die Strategie nach ihrem Symbol gefragt, und es wird zur Gruppe der verwendeten Symbole hinzugefügt. Wir haben diese Funktion in der Klasse CVolumeReceiver verwendet. Der Empfänger arbeitet dann nur mit den Symbolen, die er in sein Symbolfeld aufgenommen hat. Wir haben bereits die Einschränkung erwähnt, die sich aus diesem Verhalten ergibt.
- Wir sollten die Basisklassen CStrategy und CAdvisor so weit wie möglich bereinigen. Schreiben wir die nutzerdefinierten, abgeleiteten Klassen CVirtualStrategy und CVirtualAdvisor, um den Entwicklungszweig der EAs mit virtuellem Handel zu erstellen. Sie werden nun unsere übergeordneten Klassen für spezifische Strategien und EAs sein.
- Es ist an der Zeit, die Klasse der virtuellen Stellen zu erweitern. Jede virtuelle Position sollte einen Zeiger auf ein Empfängerobjekt erhalten, das dafür verantwortlich ist, das virtuelle Handelsvolumen auf den Markt zu bringen, und ein Handelsstrategieobjekt, das Entscheidungen über das Öffnen/Schließen einer virtuellen Position trifft. Auf diese Weise können interessierte Stellen über die Eröffnung und Schließung virtueller Stellen informiert werden.
- Verschieben wir die Speicherung aller virtuellen Positionen in ein Array, anstatt sie auf mehrere Arrays zu verteilen, die zu Strategieinstanzen gehören. Jede Strategieinstanz wird für ihre Tätigkeit mehrere Elemente aus diesem Array anfordern. Der Eigentümer des gemeinsamen Arrays wird der Empfänger des Handelsvolumens sein.
- In einem EA gibt es nur einen Empfänger. Daher werden wir es als Singleton implementieren. Seine einzige Instanz wird an allen notwendigen Stellen verfügbar sein. Wir werden diese Implementierung als die abgeleitete Klasse CVirtualReceiver formalisieren.
- Wir fügen eine Reihe neuer Entitäten - Symbolempfänger (CVirtualSymbolReceiver-Klasse) - in den Empfänger ein. Jeder Symbol-Empfänger arbeitet nur mit den virtuellen Positionen seines Symbols, das automatisch an den Symbol-Empfänger angehängt wird, wenn er geöffnet wird, und wieder gelöst wird, wenn er geschlossen wird.
Bereinigung von Basisklassen
Lassen wir nur das Wesentliche in den Basisklassen CStrategy und CAdvisor. Im Falle von CStartegy lassen wir nur die Methode für die Behandlung des OnTick-Ereignisses stehen und erhalten den folgenden prägnanten Code:
//+------------------------------------------------------------------+ //| Base class of the trading strategy | //+------------------------------------------------------------------+ class CStrategy { public: virtual void Tick() = 0; // Handle OnTick events };
Alles andere wird in den abgeleiteten der Klasse untergebracht.
Binden wir in die Basisklasse CAdvisor eine kleine Datei Macros.mqh ein, die mehrere nützliche Makros zur Durchführung von Operationen mit regulären Arrays enthält:
- APPEND(A, V) — fügt das Element V an das Ende des Arrays A an;
- FIND(A, V, I) — schreibt das Array-Element A, das gleich A ist, in die Variable I. Wird das Element nicht gefunden, so wird -1 in der Variablen I gespeichert;
- ADD(A, V) — fügt das Element V am Ende des Arrays A hinzu, wenn sich ein solches Element nicht bereits im Array befindet;
- FOREACH(A, D) — Schleife durch die Array-Indizes von A (der Index befindet sich in der lokalen Variablen i), wobei D-Aktionen im Körper ausgeführt werden;
- REMOVE_AT(A, I) — entfernt ein Element aus dem Array A an einer I-Indexposition, wobei nachfolgende Elemente verschoben und die Arraygröße verringert wird;
- REMOVE(A, V) — entfernt ein Element mit dem Wert V aus dem Array A
// Useful macros for array operations #ifndef __MACROS_INCLUDE__ #define APPEND(A, V) A[ArrayResize(A, ArraySize(A) + 1) - 1] = V; #define FIND(A, V, I) { for(I=ArraySize(A)-1;I>=0;I--) { if(A[I]==V) break; } } #define ADD(A, V) { int i; FIND(A, V, i) if(i==-1) { APPEND(A, V) } } #define FOREACH(A, D) { for(int i=0, im=ArraySize(A);i<im;i++) {D;} } #define REMOVE_AT(A, I) { int s=ArraySize(A);for(int i=I;i<s-1;i++) { A[i]=A[i+1]; } ArrayResize(A, s-1);} #define REMOVE(A, V) { int i; FIND(A, V, i) if(i>=0) REMOVE_AT(A, i) } #define __MACROS_INCLUDE__ #endif //+------------------------------------------------------------------+
Diese Makros werden in anderen Dateien verwendet, um den Code kompakter und lesbarer zu machen und den Aufruf zusätzlicher Funktionen zu vermeiden.
Wir werden alle Stellen, an denen der Empfänger angetroffen wurde, aus der CAdvisor-Klasse entfernen und nur den Aufruf der entsprechenden Strategie-Handler in der OnTick-Ereignisbehandlungsmethode belassen. Wir werden den folgenden Code erhalten:
#include "Macros.mqh" #include "Strategy.mqh" //+------------------------------------------------------------------+ //| EA base class | //+------------------------------------------------------------------+ class CAdvisor { protected: CStrategy *m_strategies[]; // Array of trading strategies public: ~CAdvisor(); // Destructor virtual void Tick(); // OnTick event handler virtual void Add(CStrategy *strategy); // Method for adding a strategy }; //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CAdvisor::~CAdvisor() { // Delete all strategy objects FOREACH(m_strategies, delete m_strategies[i]); } //+------------------------------------------------------------------+ //| OnTick event handler | //+------------------------------------------------------------------+ void CAdvisor::Tick(void) { // Call OnTick handling for all strategies FOREACH(m_strategies, m_strategies[i].Tick()); } //+------------------------------------------------------------------+ //| Strategy adding method | //+------------------------------------------------------------------+ void CAdvisor::Add(CStrategy *strategy) { APPEND(m_strategies, strategy); // Add the strategy to the end of the array } //+------------------------------------------------------------------+
Diese Klassen verbleiben in den Dateien Strategy.mqh und Advisor.mqh im aktuellen Ordner.
Verschieben wir nun den notwendigen Code in die abgeleiteten Strategie- und EA-Klassen, die mit virtuellen Positionen arbeiten sollen.
Erstellen Sie die von CStrategy geerbte Klasse CVirtualStrategy. Fügen wir ihr die folgenden Felder und Methoden hinzu:
- ein Array für die virtuellen Positionen (Aufträgen);
- Gesamtzahl der offenen Positionen und Aufträge;
- Methode zur Zählung offener virtueller Positionen und Aufträge;
- Methoden zur Handhabung des Öffnens/Schließens einer virtuellen Position (Auftrag).
#include "Strategy.mqh" #include "VirtualOrder.mqh" //+------------------------------------------------------------------+ //| Class of a trading strategy with virtual positions | //+------------------------------------------------------------------+ class CVirtualStrategy : public CStrategy { protected: CVirtualOrder *m_orders[]; // Array of virtual positions (orders) int m_ordersTotal; // Total number of open positions and orders virtual void CountOrders(); // Calculate the number of open positions and orders public: virtual void OnOpen(); // Event handler for opening a virtual position (order) virtual void OnClose(); // Event handler for closing a virtual position (order) }; //+------------------------------------------------------------------+ //| Counting open virtual positions and orders | //+------------------------------------------------------------------+ void CVirtualStrategy::CountOrders() { m_ordersTotal = 0; FOREACH(m_orders, if(m_orders[i].IsOpen()) { m_ordersTotal += 1; }) } //+------------------------------------------------------------------+ //| Event handler for opening a virtual position (order) | //+------------------------------------------------------------------+ void CVirtualStrategy::OnOpen() { CountOrders(); } //+------------------------------------------------------------------+ //| Event handler for closing a virtual position (order) | //+------------------------------------------------------------------+ void CVirtualStrategy::OnClose() { CountOrders(); }
Speichern Sie diesen Code in der Datei VirtualStrategy.mqh im aktuellen Ordner.
Da wir die Arbeit mit dem Empfänger aus der Basisklasse CAdvisor entfernt haben, sollte sie auf unsere neue Unterklasse CVirtualAdvisor übertragen werden. In dieser Klasse fügen wir das Feld m_receiver hinzu, um den Zeiger auf das Objekt des Empfängers von Handelsvolumen zu speichern.
Im Konstruktor wird das Feld mit dem Zeiger auf das einzig mögliche Empfängerobjekt initialisiert, das genau dann erstellt wird, wenn die statische Methode CVirtualReceiver::Instance() aufgerufen wird. Der Destruktor sorgt dafür, dass das Objekt ordnungsgemäß gelöscht wird.
Wir werden auch neue Aktionen in OnTick hinzufügen. Bevor wir die Ereignisbehandlung für dieses Ereignis in den Strategien starten, werden wir zuerst die Ereignisbehandlung für dieses Ereignis im Empfänger starten. Nachdem das Ereignis von den Strategien verarbeitet wurde, starten wir die Methode des Empfängers, die die offenen Volumina anpasst. Wenn der Empfänger nun der Eigentümer aller virtuellen Positionen ist, kann er selbst das Vorhandensein von Änderungen feststellen. Daher gibt es keine Implementierung der Nachverfolgung von Änderungen in der Handelsstrategieklasse, weshalb wir sie nicht nur aus der Basisstrategieklasse, sondern vollständig entfernen.
#include "Advisor.mqh" #include "VirtualReceiver.mqh" //+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: CVirtualReceiver *m_receiver; // Receiver object that brings positions to the market public: CVirtualAdvisor(ulong p_magic = 1); // Constructor ~CVirtualAdvisor(); // Destructor virtual void Tick() override; // OnTick event handler }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1) : // Initialize the receiver with a static receiver m_receiver(CVirtualReceiver::Instance(p_magic)) {}; //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CVirtualAdvisor::~CVirtualAdvisor() { delete m_receiver; // Remove the recipient } //+------------------------------------------------------------------+ //| OnTick event handler | //+------------------------------------------------------------------+ void CVirtualAdvisor::Tick(void) { // Receiver handles virtual positions m_receiver.Tick(); // Start handling in strategies CAdvisor::Tick(); // Adjusting market volumes m_receiver.Correct(); } //+------------------------------------------------------------------+
Wir speichern diesen Code in der Datei VirtualAdvisor.mqh im aktuellen Ordner.
Ausweitung der Klasse der virtuellen Positionen
Die virtuelle Positionsklasse erhält den Zeiger auf die Objekte m_receiver und m_strategy. Die Werte für diese Felder müssen über die Konstruktorparameter übergeben werden, daher werden wir auch hier Änderungen vornehmen. Ich habe auch ein paar „Getter“ für die privaten Eigenschaften der virtuellen Position hinzugefügt: Id() und Symbol(). Wir zeigen den hinzugefügten Code in der Klassenbeschreibung:
//+------------------------------------------------------------------+ //| Class of virtual orders and positions | //+------------------------------------------------------------------+ class CVirtualOrder { private: //--- Static fields... //--- Related recipient objects and strategies CVirtualReceiver *m_receiver; CVirtualStrategy *m_strategy; //--- Order (position) properties ... //--- Closed order (position) properties ... //--- Private methods public: CVirtualOrder( CVirtualReceiver *p_receiver, CVirtualStrategy *p_strategy ); // Constructor //--- Methods for checking the position (order) status ... //--- Methods for receiving position (order) properties ... ulong Id() { // ID return m_id; } string Symbol() { // Symbol return m_symbol; } //--- Methods for handling positions (orders) ... };
In der Konstruktorimplementierung haben wir einfach zwei Zeilen in die Initialisierungsliste eingefügt, um die Werte neuer Felder aus den Konstruktorparametern festzulegen:
CVirtualOrder::CVirtualOrder(CVirtualReceiver *p_receiver, CVirtualStrategy *p_strategy) : // Initialization list m_id(++s_count), // New ID = object counter + 1 m_receiver(p_receiver), m_strategy(p_strategy), ..., m_point(0) { }
Die Benachrichtigung des Empfängers und der Strategie sollte nur erfolgen, wenn eine virtuelle Position eröffnet oder geschlossen wird. Dies geschieht nur in den Methoden Open() und Close(), also fügen wir ihnen ein wenig Code hinzu:
//+------------------------------------------------------------------+ //| Open a virtual position | //+------------------------------------------------------------------+ bool CVirtualOrder::Open(...) { // If the position is already open, then do nothing ... if(s_symbolInfo.Name(symbol)) { // Select the desired symbol // Update information about current prices ... // Initialize position properties ... // Depending on the direction, set the opening price, as well as the SL and TP levels ... // Notify the recipient and the strategy that the position (order) is open m_receiver.OnOpen(GetPointer(this)); m_strategy.OnOpen(); ... return true; } return false; } //+------------------------------------------------------------------+ //| Close a position | //+------------------------------------------------------------------+ void CVirtualOrder::Close() { if(IsOpen()) { // If the position is open ... // Define the closure reason to be displayed in the log ... // Save the close price depending on the type ... // Notify the recipient and the strategy that the position (order) is open m_receiver.OnClose(GetPointer(this)); m_strategy.OnClose(); } }
Wir übergeben den Zeiger auf das aktuelle virtuelle Positionsobjekt an die OnOpen() und OnClose() Empfänger-Handler. Bei den Strategie-Handlern war dies bisher nicht erforderlich, sodass sie ohne Parameter implementiert sind.
Dieser Code verbleibt im aktuellen Ordner in der Datei mit demselben Namen — VirtualOrder.mqh.
Einführen eines neuen Empfängers
Beginnen wir mit der Implementierung der Empfängerklasse CVirtualReceiver, um die Einzigartigkeit einer Instanz einer bestimmten Klasse zu gewährleisten. Dazu werden wir ein Standardentwurfsmuster namens Singleton verwenden. Das werden wir tun müssen:
- den Klassenkonstruktor nicht-öffentlich machen;
- ein statisches Klassenfeld hinzufügen, das den Zeiger auf das Klassenobjekt speichert;
- eine statische Methode hinzufügen, die, falls nicht vorhanden, eine Instanz dieser Klasse erzeugt oder eine bereits vorhandene zurückgibt.
//+------------------------------------------------------------------+ //| Class for converting open volumes to market positions (receiver) | //+------------------------------------------------------------------+ class CVirtualReceiver : public CReceiver { protected: // Static pointer to a single class instance static CVirtualReceiver *s_instance; ... CVirtualReceiver(ulong p_magic = 0); // Private constructor public: //--- Static methods static CVirtualReceiver *Instance(ulong p_magic = 0); // Singleton - creating and getting a single instance ... }; // Initializing a static pointer to a single class instance CVirtualReceiver *CVirtualReceiver::s_instance = NULL; //+------------------------------------------------------------------+ //| Singleton - creating and getting a single instance | //+------------------------------------------------------------------+ CVirtualReceiver* CVirtualReceiver::Instance(ulong p_magic = 0) { if(!s_instance) { s_instance = new CVirtualReceiver(p_magic); } return s_instance; }
Als Nächstes fügen wir der Klasse das Array m_orders zur Speicherung aller virtuellen Positionen hinzu. Jede Strategieinstanz fordert vom Empfänger eine bestimmte Anzahl virtueller Positionen an. Wir fügen außerdem noch die statische Methode Get() hinzu, die die erforderliche Anzahl von virtuellen Positionsobjekten erzeugt und Zeiger auf diese Objekte in das Empfänger-Array und das virtuelle Positions-Array der Strategie einfügt:
class CVirtualReceiver : public CReceiver { protected: ... CVirtualOrder *m_orders[]; // Array of virtual positions ... public: //--- Static methods ... static void Get(CVirtualStrategy *strategy, CVirtualOrder *&orders[], int n); // Allocate the necessary amount of virtual positions to the strategy ... }; ... //+------------------------------------------------------------------+ //| Allocate the necessary amount of virtual positions to strategy | //+------------------------------------------------------------------+ static void CVirtualReceiver::Get(CVirtualStrategy *strategy, // Strategy CVirtualOrder *&orders[], // Array of strategy positions int n // Required number ) { CVirtualReceiver *self = Instance(); // Receiver singleton ArrayResize(orders, n); // Expand the array of virtual positions FOREACH(orders, orders[i] = new CVirtualOrder(self, strategy); // Fill the array with new objects APPEND(self.m_orders, orders[i])) // Register the created virtual position ... }
Nun ist es an der Zeit, das Array für Zeiger auf Symbolempfängerobjekte (die Klasse CVirtualSymbolReceiver ) in die Klasse aufzunehmen. Diese Klasse wurde noch nicht erstellt, aber wir wissen bereits, was sie tun soll - Marktpositionen direkt öffnen und schließen in Übereinstimmung mit virtuellen Volumen für ein einzelnes Symbol. Daher kann man sagen, dass die Anzahl der Symbolempfänger-Objekte gleich der Anzahl der verschiedenen im EA verwendeten Symbole ist. Wir leiten die Klasse von CReceiver ab, sodass sie über die Methode Correct() verfügt, die die wichtigste, nützliche Arbeit leistet. Wir werden auch die notwendigen Hilfsmethoden hinzufügen.
Lassen wir dies für später und kehren wir zur Klasse CVirtualReceiver zurück und fügen ihr die virtuelle Überschreibung der Methode Correct() hinzu.
class CVirtualReceiver : public CReceiver { protected: ... CVirtualSymbolReceiver *m_symbolReceivers[]; // Array of recipients for individual symbols public: ... //--- Public methods virtual bool Correct() override; // Adjustment of open volumes };
Die Implementierung der Methode Correct() ist nun recht einfach, da wir die Hauptarbeit auf eine niedrigere Ebene der Hierarchie verlagern. Im Moment reicht es aus, eine Schleife durch alle Symbolempfänger zu ziehen und deren Methode Correct() aufzurufen.
Um die Anzahl der unnötigen Aufrufe zu reduzieren, fügen wir eine vorläufige Prüfung hinzu, dass der Handel nun generell erlaubt ist, indem wir die Methode IsTradeAllowed() hinzufügen, die die Frage beantwortet. Wir werden auch das Klassenfeld m_isChanged hinzufügen, das als Flag für Änderungen in offenen virtuellen Positionen dienen soll. Wir werden sie auch überprüfen, bevor wir eine Anpassung verlangen.
class CVirtualReceiver : public CReceiver { ... bool m_isChanged; // Are there any changes in open positions? ... bool IsTradeAllowed(); // Is trading available? public: ... virtual bool Correct() override; // Adjustment of open volumes }; //+------------------------------------------------------------------+ //| Adjust open volumes | //+------------------------------------------------------------------+ bool CVirtualReceiver::Correct() { bool res = true; if(m_isChanged && IsTradeAllowed()) { // If there are changes, then we call the adjustment of the recipients of individual symbols FOREACH(m_symbolReceivers, res &= m_symbolReceivers[i].Correct()); m_isChanged = !res; } return res; }
Wir überprüfen in der Methode IsTradeAllowed() den Status des Terminals und des Handelskontos, um festzustellen, ob ein echter Handel möglich ist:
//+------------------------------------------------------------------+ //| Is trading available? | //+------------------------------------------------------------------+ bool CVirtualReceiver::IsTradeAllowed() { return (true && MQLInfoInteger(MQL_TRADE_ALLOWED) && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && AccountInfoInteger(ACCOUNT_TRADE_EXPERT) && AccountInfoInteger(ACCOUNT_TRADE_ALLOWED) && TerminalInfoInteger(TERMINAL_CONNECTED) ); }
Wir haben das Änderungs-Flag in der Methode Correct() gesetzt. Das Flag wird zurückgesetzt, wenn die Volumensanpassung erfolgreich war. Aber wo soll dieses Flag gesetzt werden? Dies sollte natürlich geschehen, wenn eine virtuelle Position geöffnet oder geschlossen wird. In der Klasse CVirtualOrder haben wir speziell die Aufrufe für die Methoden OnOpen() und OnClose(), die in der Klasse CVirtualReceiver noch nicht vorhanden sind, zu den open/close-Methoden hinzugefügt. Wir werden in ihnen das Flag für Änderungen setzen.
Darüber hinaus sollten wir den gewünschten Symbolempfänger über die Änderungen in diesen Handlern informieren. Beim Öffnen der allerersten, virtuellen Position für ein bestimmtes Symbol gibt es den entsprechenden Symbolempfänger noch nicht, sodass wir ihn erstellen und informieren müssen. Bei späteren Operationen zum Öffnen/Schließen virtueller Positionen für ein bestimmtes Symbol gibt es bereits einen entsprechenden Symbol-Empfänger, sodass wir diesen nur noch informieren müssen.
class CVirtualReceiver : public CReceiver { ... public: ... //--- Public methods void OnOpen(CVirtualOrder *p_order); // Handle virtual position opening void OnClose(CVirtualOrder *p_order); // Handle virtual position closing ... }; //+------------------------------------------------------------------+ //| Handle opening a virtual position | //+------------------------------------------------------------------+ void CVirtualReceiver::OnOpen(CVirtualOrder *p_order) { string symbol = p_order.Symbol(); // Define position symbol CVirtualSymbolReceiver *symbolReceiver; int i; FIND(m_symbolReceivers, symbol, i); // Search for the symbol recipient if(i == -1) { // If not found, then create a new recipient for the symbol symbolReceiver = new CVirtualSymbolReceiver(m_magic, symbol); // and add it to the array of symbol recipients APPEND(m_symbolReceivers, symbolReceiver); } else { // If found, then take it symbolReceiver = m_symbolReceivers[i]; } symbolReceiver.Open(p_order); // Notify the symbol recipient about the new position m_isChanged = true; // Remember that there are changes } //+------------------------------------------------------------------+ //| Handle closing a virtual position | //+------------------------------------------------------------------+ void CVirtualReceiver::OnClose(CVirtualOrder *p_order) { string symbol = p_order.Symbol(); // Define position symbol int i; FIND(m_symbolReceivers, symbol, i); // Search for the symbol recipient if(i != -1) { m_symbolReceivers[i].Close(p_order); // Notify the symbol recipient about closing a position m_isChanged = true; // Remember that there are changes } }
Zusätzlich zum Öffnen/Schließen von virtuellen Positionen auf der Grundlage von Handelsstrategiesignalen können diese geschlossen werden, wenn StopLoss- oder TakeProfit-Levels erreicht werden. In der Klasse CVirtualOrder gibt es speziell dafür die Methode Tick(). Er prüft die Level und schließt die virtuelle Position, falls erforderlich. Sie sollte bei jedem Tick und für alle virtuellen Positionen aufgerufen werden. Dies ist genau das, was die Methode Tick() in der Klasse CVirtualReceiver tun wird. Fügen wir die Klasse hinzu:
class CVirtualReceiver : public CReceiver { ... public: ... //--- Public methods void Tick(); // Handle a tick for the array of virtual orders (positions) ... }; //+------------------------------------------------------------------+ //| Handle a tick for the array of virtual orders (positions) | //+------------------------------------------------------------------+ void CVirtualReceiver::Tick() { FOREACH(m_orders, m_orders[i].Tick()); }
Achten wir am Ende darauf, den für virtuelle Positionsobjekte zugewiesenen Speicher korrekt wieder freizugeben. Da sie sich alle in dem Array m_orders befinden, fügen wir einen Destruktor hinzu, in dem wir sie löschen werden:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CVirtualReceiver::~CVirtualReceiver() { FOREACH(m_orders, delete m_orders[i]); // Remove virtual positions }
Wir speichern den resultierenden Code in der Datei VirtualReceiver.mqh im aktuellen Ordner.
Implementierung eines Symbolempfängers
Es bleibt, die letzte Klasse CVirtualSymbolReceiver zu implementieren, damit die Betriebsart eine fertige, gebrauchstaugliche Form annimmt. Wir werden den Hauptinhalt von der Klasse CVolumeReceiver aus dem vorherigen Artikel übernehmen, wobei wir die Stellen entfernen, die sich auf die Bestimmung des Symbols jeder virtuellen Position und die Aufzählung der Symbole während der Anpassung beziehen.
Objekte dieser Klasse haben auch ihre eigenen Arrays mit Zeigern auf virtuelle Positionsobjekte, aber hier ändert sich ihre Zusammensetzung ständig. Wir verlangen, dass dieses Array nur offene virtuelle Positionen enthält. Dann wird klar, was beim Öffnen und Schließen einer virtuellen Position zu tun ist: Sobald die virtuelle Position geöffnet wird, sollten wir sie dem Array des entsprechenden Symbolempfängers hinzufügen, und sobald sie geschlossen wird, sollten wir sie aus dem Array entfernen.
Es wäre auch praktisch, wenn wir ein Flag für Änderungen in der Zusammensetzung der offenen virtuellen Positionen hätten. Auf diese Weise können unnötige Kontrollen bei jedem Tick vermieden werden.
Fügen wir der Klasse Felder für ein Symbol, ein Array von Positionen und eine Änderungsmarkierung sowie zwei Methoden für das Öffnen/Schließen hinzu:
class CVirtualSymbolReceiver : public CReceiver { string m_symbol; // Symbol CVirtualOrder *m_orders[]; // Array of open virtual positions bool m_isChanged; // Are there any changes in the composition of virtual positions? ... public: ... void Open(CVirtualOrder *p_order); // Register opening a virtual position void Close(CVirtualOrder *p_order); // Register closing a virtual position ... };
Die Implementierung dieser Methoden selbst ist trivial: Wir fügen die übergebene virtuelle Position dem Array hinzu bzw. entfernen sie daraus und setzen das Flag für das Vorhandensein von Änderungen.
//+------------------------------------------------------------------+ //| Register opening a virtual position | //+------------------------------------------------------------------+ void CVirtualSymbolReceiver::Open(CVirtualOrder *p_order) { APPEND(m_orders, p_order); // Add a position to the array m_isChanged = true; // Set the changes flag } //+------------------------------------------------------------------+ //| Register closing a virtual position | //+------------------------------------------------------------------+ void CVirtualSymbolReceiver::Close(CVirtualOrder *p_order) { REMOVE(m_orders, p_order); // Remove a position from the array m_isChanged = true; // Set the changes flag }
Außerdem müssen wir den gewünschten Symbolempfänger über einen Symbolnamen suchen. Um den normalen linearen Suchalgorithmus aus dem Makro FIND(A,V,I) zu verwenden, fügen wir einen überladenen Operator hinzu, der den Empfänger des Symbols mit der Zeichenkette vergleicht und „wahr“ zurückgibt, wenn das Instanzsymbol mit der übergebenen Zeichenkette übereinstimmt:
class CVirtualSymbolReceiver : public CReceiver { ... public: ... bool operator==(const string symbol) {// Operator for comparing by a symbol name return m_symbol == symbol; } ... };
Hier finden Sie eine vollständige Beschreibung der Klasse CVirtualSymbolReceiver. Die spezifische Implementierung aller Methoden finden Sie in den beigefügten Dateien.
class CVirtualSymbolReceiver : public CReceiver { string m_symbol; // Symbol CVirtualOrder *m_orders[]; // Array of open virtual positions bool m_isChanged; // Are there any changes in the composition of virtual positions? bool m_isNetting; // Is this a netting account? double m_minMargin; // Minimum margin for opening CPositionInfo m_position; // Object for obtaining properties of market positions CSymbolInfo m_symbolInfo; // Object for getting symbol properties CTrade m_trade; // Object for performing trading operations double MarketVolume(); // Volume of open market positions double VirtualVolume(); // Volume of open virtual positions bool IsTradeAllowed(); // Is trading by symbol available? // Required volume difference double DiffVolume(double marketVolume, double virtualVolume); // Volume correction for the required difference bool Correct(double oldVolume, double diffVolume); // Auxiliary opening methods bool ClearOpen(double diffVolume); bool AddBuy(double volume); bool AddSell(double volume); // Auxiliary closing methods bool CloseBuyPartial(double volume); bool CloseSellPartial(double volume); bool CloseHedgingPartial(double volume, ENUM_POSITION_TYPE type); bool CloseFull(); // Check margin requirements bool FreeMarginCheck(double volume, ENUM_ORDER_TYPE type); public: CVirtualSymbolReceiver(ulong p_magic, string p_symbol); // Constructor bool operator==(const string symbol) {// Operator for comparing by a symbol name return m_symbol == symbol; } void Open(CVirtualOrder *p_order); // Register opening a virtual position void Close(CVirtualOrder *p_order); // Register closing a virtual position virtual bool Correct() override; // Adjustment of open volumes };
Wir speichern diesen Code in der Datei VirtualSymbolReceiver.mqh im aktuellen Ordner.
Vergleich der Ergebnisse
Die sich daraus ergebende Betriebsart kann wie folgt dargestellt werden:
Abb. 3. Betriebsart aus dem aktuellen Artikel
Jetzt kommt der interessanteste Teil. Lassen Sie uns den EA kompilieren, der neun Instanzen von Strategien mit den gleichen Parametern wie im vorherigen Artikel verwendet. Führen wir Testläufe mit einem ähnlichen EA aus dem vorherigen Artikel und dem, den wir gerade zusammengestellt haben, durch:
Abb. 4. Ergebnisse des EA aus dem vorherigen Artikel
Abb. 5. Ergebnisse der EA aus dem aktuellen Artikel
Im Allgemeinen sind die Ergebnisse nahezu identisch. Die Saldenkurven sind im Allgemeinen ununterscheidbar. Kleine Unterschiede, die in den Berichten sichtbar werden, können auf verschiedene Gründe zurückzuführen sein und werden weiter analysiert.
Bewertung des weiteren Potenzials
In der Diskussion des vorangegangenen Artikels wurde eine logische Frage gestellt: Welches sind die attraktivsten Handelsergebnisse, die mit dem fraglichen Ansatz erzielt werden können? Bisher haben die Grafiken eine Rendite von 20 % über 5 Jahre ergeben, was nicht besonders attraktiv erscheint.
Die Antwort auf diese Frage kann vorerst wie folgt lauten. Erstens müssen die Ergebnisse, die auf die gewählten einfachen Strategien zurückzuführen sind, und die Ergebnisse, die sich aus der Umsetzung der gemeinsamen Arbeit ergeben, klar voneinander getrennt werden.
Die Ergebnisse der ersten Kategorie werden sich ändern, wenn eine einfache Strategie durch eine andere ersetzt wird. Es liegt auf der Hand, dass das Gesamtergebnis umso besser ausfällt, je besser die Ergebnisse der einzelnen Instanzen einfacher Strategien sind. Die hier vorgestellten Ergebnisse wurden mit einer Handelsidee erzielt, die zunächst genau durch ihre Qualität und Eignung bestimmt wurde. Wir bewerten diese Ergebnisse einfach anhand des Verhältnisses zwischen Gewinn und Verlust für das Testintervall.
Die Ergebnisse der zweiten Kategorie sind die vergleichenden Ergebnisse der gemeinsamen und der Einzelarbeit. Hier wird die Bewertung auf der Grundlage anderer Parameter vorgenommen: Verbesserung der Linearität der Wachstumskurve des Eigenkapitals, Verringerung des Drawdowns und andere. Diese Ergebnisse scheinen wichtiger zu sein, denn es besteht die Hoffnung, mit ihrer Hilfe die nicht besonders hervorragenden Ergebnisse der ersten Kategorie auf ein akzeptables Niveau zu bringen.
Für alle Ergebnisse ist es jedoch ratsam, zunächst den Handel mit variablen Lots einzuführen. Andernfalls ist es schwieriger, das Verhältnis zwischen Rentabilität und Ausfall auf der Grundlage von Testergebnissen abzuschätzen, obwohl dies immer noch möglich ist.
Versuchen wir, eine kleine Ersteinlage zu nehmen und einen neuen optimalen Wert für die Größe der offenen Positionen für den maximal zulässigen Drawdown von 50% für einen Zeitraum von 5 Jahren (2018.01.01 — 2023.01.01) zu wählen. Nachfolgend finden Sie die Ergebnisse der EA-Läufe aus diesem Artikel mit einem anderen Positionsgrößenmultiplikator, aber konstant für alle fünf Jahre mit einer anfänglichen Einlage von 1000 USD. Im vorangegangenen Artikel wurden die Positionsgrößen auf die Einlagengröße von 10.000 USD kalibriert, sodass der anfängliche Wert von depoPart_ um etwa das Zehnfache reduziert wurde.
Abb. 6. Testergebnisse mit verschiedenen Positionsgrößen
Wir sehen, dass der EA bei minimalem depoPart_= 0,04 keine echten Positionen eröffnet hat, da ihr Volumen bei der Neuberechnung im Verhältnis zum Saldo weniger als 0,01 beträgt. Aber ab dem nächsten Multiplikatorwert depoPart_= 0,06 wurden Marktpositionen eröffnet.
Bei einem maximalen depoPart_ = 0,4 ergibt sich ein Gewinn von rund 22.800 USD. Der hier gezeigte Drawdown ist jedoch der relative Drawdown, der während der gesamten Laufzeit auftritt. Aber 10% von 23.000 und 1000 sind sehr unterschiedliche Werte. Daher sollten wir uns unbedingt die Ergebnisse eines einzelnen Laufs ansehen:
Abb. 7. Testergebnisse bei maximalem depoPart_ = 0,4
Wie Sie sehen können, wurde der Drawdown von 1167 USD tatsächlich erreicht, was zum Zeitpunkt des Erreichens nur 9,99% des aktuellen Guthabens ausmachte, aber wenn der Beginn des Testzeitraums unmittelbar vor diesem unangenehmen Moment gelegen hätte, dann hätten wir die gesamte Einlage verloren. Daher können wir diese Positionsgröße nicht verwenden.
Schauen wir uns die Ergebnisse an, wenn depoPart_ = 0,2
Abb. 8. Testergebnisse bei depoPart_ = 0,2
In diesem Fall überstieg der maximale Drawdown nicht 494 USD, d. h. etwa 50 % der ursprünglichen Einlage von 1000 USD. Bei einer solchen Positionsgröße kommt es also selbst dann nicht zum Verlust der gesamten Einlage, wenn der Beginn des Zeitraums während der betrachteten fünf Jahre so schlecht wie möglich gewählt wird.
Bei dieser Positionsgröße werden die Testergebnisse für 1 Jahr (2022) wie folgt aussehen:
Abb. 9. Testergebnisse für 2022 bei depoPart_ = 0,2
Bei einem erwarteten maximalen Drawdown von etwa 50 % ergibt sich also ein Gewinn von etwa 150 % pro Jahr.
Diese Ergebnisse sehen ermutigend aus, aber es gibt einen Haken an der Sache. Die Ergebnisse für das Jahr 2023, das nicht in die Parameteroptimierung einbezogen wurde, fallen deutlich schlechter aus:
Abb. 10. Testergebnisse für 2023 bei depoPart_ = 0,2
Natürlich haben wir am Ende des Jahres die 40 % Gewinn in den Testergebnissen erhalten, aber es gab kein nachhaltiges Wachstum in 8 von 12 Monaten. Dieses Problem scheint das Hauptproblem zu sein, und diese Artikelserie wird sich mit verschiedenen Lösungsansätzen befassen.
Schlussfolgerung
In diesem Artikel haben wir uns auf die weitere Entwicklung des Codes vorbereitet, indem wir den Code aus dem vorherigen Teil vereinfacht und optimiert haben. Wir haben einige zuvor festgestellte Mängel behoben, die unsere Fähigkeit, verschiedene Handelsstrategien zu nutzen, einschränken könnten. Die Testergebnisse zeigten, dass die neue Implementierung nicht schlechter funktioniert als die vorherige. Die Geschwindigkeit der Umsetzung blieb unverändert, aber es ist möglich, dass der Zuwachs erst bei einer mehrfachen Erhöhung der Anzahl der Strategieinstanzen auftritt.
Dazu müssen wir endlich herausfinden, wie wir die Eingabeparameter der Strategien speichern, wie wir sie in Parameterbibliotheken kombinieren und wie wir die besten Kombinationen aus denjenigen auswählen, die sich als Ergebnis der Optimierung einzelner Strategieinstanzen ergeben.
Wir werden die Arbeit in der gewählten Richtung im nächsten Artikel fortsetzen.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/14148





- 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.