![Entwicklung eines Expertenberaters für mehrere Währungen (Teil 2): Übergang zu virtuellen Positionen von Handelsstrategien](https://c.mql5.com/2/69/Developing_a_multi-currency_advisor_5Part_2f_Transition_to_virtual_positions_of_trading_strategies_6.jpg)
Entwicklung eines Expertenberaters für mehrere Währungen (Teil 2): Übergang zu virtuellen Positionen von Handelsstrategien
Einführung
Im letzten Artikel haben wir mit der Entwicklung eines Multiwährungs-EAs begonnen, der gleichzeitig mit verschiedenen Handelsstrategien arbeitet. In der ersten Phase gab es nur zwei verschiedene Strategien. Sie stellten die Umsetzung derselben Handelsidee dar und arbeiteten mit demselben Handelsinstrument (Symbol) und Chart-Zeitrahmen (Periode). Sie unterschieden sich nur in den numerischen Werten der Parameter voneinander.
Außerdem haben wir die optimale Größe der offenen Positionen auf der Grundlage des gewünschten maximalen Drawdown-Niveaus (10 % der Einlage) bestimmt. Wir haben dies für jede Strategie separat getan. Als wir die beiden Strategien miteinander kombinierten, mussten wir die Größe der eröffneten Positionen reduzieren, um das vorgegebene Drawdown-Niveau zu halten. Bei zwei Strategien war der Rückgang gering. Was aber, wenn wir Dutzende oder Hunderte von Strategieinstanzen kombinieren wollen? Es kann durchaus vorkommen, dass wir bei einigen Strategien die Positionsgröße auf einen Wert reduzieren müssen, der unter der vom Broker erlaubten Mindestgröße für offene Positionen liegt. In diesem Fall sind diese Strategien einfach nicht in der Lage, am Handel teilzunehmen. Wie können wir sie zum Funktionieren bringen?
Zu diesem Zweck nehmen wir den Strategien das Recht, selbstständig Positionen zu eröffnen und schwebende Aufträge zu erteilen. Die Strategien müssen nur noch virtuell handeln, d. h. sich merken, auf welchen Niveaus Positionen einer bestimmten Größe eröffnet werden sollten, und auf Anfrage melden, welches Volumen jetzt eröffnet werden sollte. Wir eröffnen echte Marktpositionen erst, nachdem wir alle Strategien geprüft und das erforderliche Gesamtvolumen berechnet haben, wobei wir die Skalierung berücksichtigen, um einen bestimmten Drawdown aufrechtzuerhalten.
Uns geht es jetzt nur darum, die Eignung dieses Ansatzes zu prüfen, nicht aber die Effizienz seiner Umsetzung. Daher werden wir im Rahmen dieses Artikels versuchen, zumindest eine funktionierende Implementierung dieses Ansatzes zu entwickeln, die uns später dabei helfen wird, einen aus architektonischer Sicht schöneren Ansatz zu entwickeln, da wir bereits wissen, wie wir Fehler vermeiden können.
Versuchen wir, dies umzusetzen.
Rückblick auf frühere Errungenschaften
Wir haben die EA-Klasse CAdvisor entwickelt, die das Array der Handelsstrategie-Instanzen (genauer gesagt, Zeiger auf Instanzen) speichert. Dies ermöglicht es, eine Instanz eines Expert Advisors im Hauptprogramm zu erstellen und ihm mehrere Instanzen von Strategieklassen hinzuzufügen. Da das Array Zeiger auf Objekte der Basisklasse CStrategy speichert, kann es Zeiger auf Objekte aller von CStrategy geerbten Nachfolgeklassen speichern. In unserem Fall haben wir eine abgeleitete Klasse CSimpleVolumesStrategy erstellt, deren zwei Objekte zu diesem Array im EA hinzugefügt wurden.
Wir sollten uns auf geeignete Namen einigen, um die Präsentation zu erleichtern:
- Der EA ist unsere endgültige mq5-Datei, die nach der Kompilierung eine ausführbare ex5-Datei liefert, die im Prüfprogramm und im Terminal ausgeführt werden kann.
- Der EA ist das im Programm deklarierte Objekt der Klasse CAdvisor. Wir werden nur eine EA-Instanz in einem Programm verwenden.
- Die Strategie ist ein Objekt einer Unterklasse, die von der Basisklasse CStrategy geerbt wurde.
Erinnern wir uns auch daran, dass ein Zeiger auf ein Objekt (Strategie oder eine andere Klasse) eine Information über den Speicherplatz eines zuvor erstellten Objekts ist (vereinfacht). So kann vermieden werden, dass dasselbe Objekt an einem anderen Speicherort neu erstellt wird, wenn es an Funktionen übergeben oder neue Variablen oder Array-Elemente zugewiesen werden.
Aus diesem Grund werden im Expert Advisor Zeiger auf Strategieobjekte in einem Array gespeichert, sodass beim Füllen dieses Arrays keine Kopien von Strategieobjekten erstellt werden. Wenn wir dann auf die Elemente des Strategie-Arrays zugreifen, greifen wir auf die ursprünglichen Strategieobjekte zu.
Die Arbeiten der EA umfassten die folgenden Phasen:
- Der EA wurde im Speicherbereich „static“ erstellt.
- Bei der Initialisierung des Programms wurden zwei Strategien im dynamischen Speicher angelegt und Zeiger auf sie im EA gespeichert.
- Während das Programm lief, rief der EA nacheinander jede Strategie auf, um die erforderlichen Handelsaktionen durch Aufruf der Methode CStrategy::Tick() durchzuführen.
- Bei der Deinitialisierung des Programms löschte der EA Strategieobjekte aus dem dynamischen Speicher.
Bevor wir beginnen, müssen wir noch einige kleinere Korrekturen am EA und der EA-Klasse vornehmen. In der EA werden wir dafür sorgen, dass der Experte im dynamischen Speicherbereich erstellt wird.
CAdvisor expert; // EA object CAdvisor *expert; // Pointer to the EA object int OnInit() { expert = new CAdvisor(); // Create EA object // The rest of the code from OnInit() ... }
In der EA-Klasse werden wir einen Destruktor erstellen - eine Funktion, die automatisch aufgerufen wird, wenn das EA-Objekt aus dem Speicher gelöscht wird. Der Destruktor übernimmt die Operationen zum Entfernen von Strategieobjekten der Methode CAdvisor::Deinit() aus dem dynamischen Speicher. Wir brauchen diese Methode jetzt nicht. Streichen wir sie. Wir werden auch die Klassenvariable m_strategiesCount entfernen, die die Anzahl der Strategien speichert. Bei Bedarf können wir ArraySize() verwenden.
class CAdvisor { protected: CStrategy *m_strategies[]; // Array of trading strategies public: ~CAdvisor(); // Destructor // ... }; //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CAdvisor::~CAdvisor() { // Delete all strategy objects for(int i = 0; i < ArraySize(m_strategies); i++) { delete m_strategies[i]; } }
Wir ersetzen in der Funktion OnDeinit() die Methode CAdvisor::Deinit() durch das Entfernen des EA-Objekts.
void OnDeinit(const int reason) { expert.Deinit(); delete expert; }
Strategieplan
Wenn Handelsstrategien nicht mehr in der Lage sind, selbst Marktpositionen zu eröffnen, dann
- benötigen wir Objekte, die Informationen über virtuelle Positionen von Strategien speichern;
- brauchen wir Objekte, die Informationen über virtuelle Positionen in reale Marktpositionen umwandeln können.
Objekte für virtuelle Positionen sollten fester Bestandteil der Strategie sein, und es sollte mehrere davon geben. Nennen wir also die erste neue Klasse CVirtualOrder und fügen wir das Array dieser Objekte der Strategieklasse CStrategy hinzu. CStrategy erhält außerdem eine Eigenschaft, die eine Anzeige der Änderungen der offenen virtuellen Positionen speichert, sowie Methoden zum Abrufen und Setzen ihres Wertes. Diese Eigenschaft bestimmt, in welchem von zwei Zuständen sich die Strategie gerade befindet:
- keine Änderungen - das gesamte offene virtuelle Volumen wurde für den Markt freigegeben;
- es gibt Änderungen - das virtuelle Volumen entspricht nicht dem Marktvolumen, daher ist es notwendig, das Volumen der realen Marktpositionen anzupassen.
Da nun jemand anderes für die Eröffnung echter Positionen zuständig ist, kann die Magic-Nummer m_magic aus der Basisklasse der Strategie entfernt werden. In Zukunft werden wir die grundlegendste Klasse von Strategien noch weiter bereinigen, doch vorerst beschränken wir uns auf eine Teilbereinigung.
Damit sieht die Basisstrategieklasse nun wie folgt aus.
#include "VirtualOrder.mqh" //+------------------------------------------------------------------+ //| Base class of the trading strategy | //+------------------------------------------------------------------+ class CStrategy { protected: string m_symbol; // Symbol (trading instrument) ENUM_TIMEFRAMES m_timeframe; // Chart period (timeframe) double m_fixedLot; // Size of opened positions (fixed) CVirtualOrder m_orders[]; // Array of virtual positions (orders) int m_ordersTotal; // Total number of open positions and orders double m_volumeTotal; // Total volume of open positions and orders bool m_isChanged; // Sign of changes in open virtual positions void CountOrders(); // Calculate the number and volumes of open positions and orders public: // Constructor CStrategy(string p_symbol = "", ENUM_TIMEFRAMES p_timeframe = PERIOD_CURRENT, double p_fixedLot = 0.01); virtual void Tick(); // Main method - handling OnTick events virtual double Volume(); // Total volume of virtual positions virtual string Symbol(); // Strategy symbol (only one for a single strategy so far) virtual bool IsChanged(); // Are there any changes in open virtual positions? virtual void ResetChanges(); // Reset the sign of changes in open virtual positions };
Wir können bereits die Methoden Symbol(), IsChanged() und ResetChanges() implementieren.
//+------------------------------------------------------------------+ //| Strategy symbol | //+------------------------------------------------------------------+ string CStrategy::Symbol() { return m_symbol; } //+------------------------------------------------------------------+ //| Are there any changes to open virtual positions? | //+------------------------------------------------------------------+ bool CStrategy::IsChanged() { return m_isChanged; } //+------------------------------------------------------------------+ //| Reset the flag for changes in virtual positions | //+------------------------------------------------------------------+ void CStrategy::ResetChanges() { m_isChanged = false; }
Die übrigen Methoden ( Tick(), Volume() und CountOrders()) werden wir später entweder in den Nachkommen der Basisklasse oder in der Klasse selbst implementieren.
Die zweite neue Klasse, deren Objekte daran beteiligt sein werden, virtuelle Positionen von Strategien auf den Markt zu bringen, wird CReceiver heißen. Um zu funktionieren, sollte dieses Objekt Zugang zu allen EA-Strategien haben, um herauszufinden, welche Symbole und welches Volumen für die Eröffnung echter Positionen verwendet werden sollten. Ein solches Objekt reicht für einen EA aus. Das CReceiver-Objekt sollte eine Magische-Nummer haben, die für geöffnete Marktpositionen gesetzt wird.
#include "Strategy.mqh" //+------------------------------------------------------------------+ //| Base class for converting open volumes into market positions | //+------------------------------------------------------------------+ class CReceiver { protected: CStrategy *m_strategies[]; // Array of strategies ulong m_magic; // Magic public: CReceiver(ulong p_magic = 0); // Constructor virtual void Add(CStrategy *strategy); // Adding strategy virtual bool Correct(); // Adjustment of open volumes }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CReceiver::CReceiver(ulong p_magic) : m_magic(p_magic) { ArrayResize(m_strategies, 0, 128); } //+------------------------------------------------------------------+ //| Add strategy | //+------------------------------------------------------------------+ void CReceiver::Add(CStrategy *strategy) { APPEND(m_strategies, strategy); } //+------------------------------------------------------------------+ //| Adjust open volumes | //+------------------------------------------------------------------+ bool CReceiver::Correct() { return true; }
Diese Basisklasse enthält keine Implementierung eines speziellen Mechanismus zur Volumenregelung. Daher werden wir in der Lage sein, verschiedene Implementierungen der Anpassung in verschiedenen Nachkommen dieser Klasse vorzunehmen. Das Klassenobjekt kann als ein einfacher Anfang für die Strategien dienen, die vorerst selbst Marktpositionen eröffnen. Wir benötigen dies, um den Anpassungsmechanismus zu debuggen: Es wird notwendig sein, zu vergleichen, welche Positionen vom EA eröffnet werden, bei denen die Strategien selbst realen Handel betreiben, und welche Positionen vom EA eröffnet werden, bei denen die Strategien nur virtuellen Handel betreiben.
Daher werden wir zwei EAs vorbereiten, in denen die Strategien aus dem vorherigen Artikel selbst realen Handel betreiben.
Im ersten EA wird die Strategie in einer Instanz sein. In seinen Parametern können wir die Parameter dieser einzelnen Strategieinstanz für ihre Optimierung angeben.
Der zweite EA enthält mehrere Instanzen von Handelsstrategien mit vordefinierten Parametern, die aus der ersten EA-Optimierung resultieren.
EA zur Optimierung der Strategieparameter
Beim letzten Mal haben wir die Strategieparameter optimiert, indem wir die Strategieimplementierung nicht in Form des Klassenobjekts CStrategy verwendet haben. Aber jetzt haben wir bereits eine fertige Klasse CSimpleVolumesStrategy, also erstellen wir ein separates Programm, in dem der EA eine einzelne Instanz dieser Strategie enthalten wird. Wir werden diese Klasse etwas anders nennen, um zu betonen, dass die Strategie selbst Marktpositionen eröffnet: Statt CSimpleVolumesStrategy verwenden wir CSimpleVolumesMarketStrategy und speichern sie in der Datei SimpleVolumesMarketStrategy.mqh im aktuellen Ordner.
In der EA-Datei werden wir die Strategieparameter aus den EA-Eingabevariablen laden und eine Instanz der Strategie zum EA-Objekt hinzufügen. Wir erhalten einen EA, mit dem wir die Strategieparameter optimieren können.
#include "Advisor.mqh" #include "SimpleVolumesMarketStrategy.mqh" #include "VolumeReceiver.mqh" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ input string symbol_ = "EURGBP"; // Trading instrument (symbol) input ENUM_TIMEFRAMES timeframe_ = PERIOD_H1; // Chart period input group "=== Opening signal parameters" input int signalPeriod_ = 130; // Number of candles for volume averaging input double signalDeviation_ = 0.9; // Relative deviation from the average to open the first order input double signaAddlDeviation_ = 1.4; // Relative deviation from the average for opening the second and subsequent orders input group "=== Pending order parameters" input int openDistance_ = 0; // Distance from price to pending order input double stopLevel_ = 2000; // Stop Loss (in points) input double takeLevel_ = 475; // Take Profit (in points) input int ordersExpiration_ = 6000; // Pending order expiration time (in minutes) input group "=== Money management parameters" input int maxCountOfOrders_ = 3; // Maximum number of simultaneously open orders input double fixedLot_ = 0.01; // Single order volume input group "=== EA parameters" input ulong magic_ = 27181; // Magic CAdvisor *expert; // Pointer to the EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { expert = new CAdvisor(); expert.Add(new CSimpleVolumesMarketStrategy( magic_, symbol_, timeframe_, fixedLot_, signalPeriod_, signalDeviation_, signaAddlDeviation_, openDistance_, stopLevel_, takeLevel_, ordersExpiration_, maxCountOfOrders_) ); // Add one strategy instance return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { expert.Tick(); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { delete expert; }
Speichern wir sie im aktuellen Ordner der Datei SimpleVolumesMarketExpertSingle.mq5.
Lassen Sie uns nun die Handelsstrategie ein wenig verkomplizieren, um die Umsetzung der Aufgabe zu vereinfachen. Es wird für uns einfacher sein, eine Strategie auf den virtuellen Handel zu übertragen, die Marktpositionen anstelle von schwebenden Aufträgen verwendet. Die aktuelle Version der Strategie funktioniert nur mit schwebenden Aufträgen. Fügen wir der Strategie openDistance_ eine Parameterwertanalyse hinzu. Liegt er über Null, eröffnet die Strategie die schwebenden Aufträge BUY_STOP und SELL_STOP. Liegt er unter Null, so eröffnet die Strategie die schwebenden Aufträge BUY_LIMIT und SELL_LIMIT. Ist der Wert gleich Null, werden Marktpositionen eröffnet.
Dazu müssen wir lediglich Änderungen am Code der Methoden CSimpleVolumesMarketStrategy::OpenBuyOrder() undCSimpleVolumesMarketStrategy::OpenSellOrder() vornehmen.
void CSimpleVolumesMarketStrategy::OpenBuyOrder() { // Previous code in the method ... // Order volume double lot = m_fixedLot; // Set a pending order bool res = false; if(openDistance_ > 0) { res = trade.BuyStop(lot, ...); } else if(openDistance_ < 0) { res = trade.BuyLimit(lot, ...); } else { res = trade.Buy(lot, ...); } if(!res) { Print("Error opening order"); } }
Eine weitere notwendige Änderung, die an der Strategie vorgenommen werden muss, besteht darin, den Initialisierungscode von der Methode Init() in den Strategiekonstruktor zu verschieben. Dies ist notwendig, da der EA nun nicht mehr die Strategieinitialisierungsmethode aufruft, da er davon ausgeht, dass sich sein Code innerhalb des Strategiekonstruktors befindet.
Lassen Sie uns einen neuen EA kompilieren und ihn so einstellen, dass er auf H1 mit drei Symbolen optimiert: EURGBP, GBPUSD und EURUSD.
Abb. 1. Testergebnisse mit den Parametern [EURGBP, H1, 17, 1.7, 0.5, 0, 16500, 100, 52000, 3, 0.01].
Wählen wir aus den Optimierungsergebnissen mehrere gute Optionen für Parameter aus (z. B. drei Optionen für jedes Symbol) und erstellen einen zweiten EA, in dem neun Instanzen der Strategie mit den ausgewählten Parametern erstellt werden. Für jeden Fall berechnen wir die optimale Größe der offenen Positionen, bei der der Drawdown einer Strategie 10 % nicht überschreitet. Die Berechnungsmethode wurde im vorigen Artikel beschrieben.
Um Veränderungen in der EA-Leistung zu demonstrieren, werden wir die Möglichkeit schaffen, die einzubeziehenden Strategien festzulegen. Zu diesem Zweck werden zunächst alle Strategieinstanzen in das Array aus neun Elementen eingefügt. Fügen wir die Eingabe startIndex_ hinzu, die den Anfangsindex im Strategie-Array festlegt, von dem aus die Strategien zu arbeiten beginnen. Der Parameter totalStrategies_ bestimmt, wie viele sequentielle Strategien aus dem Array ab startIndex_ gestartet werden sollen. Am Ende der Initialisierung fügen wir dem EA-Objekt die entsprechenden Strategien aus dem Array hinzu.
#include "Advisor.mqh" #include "SimpleVolumesMarketStrategy.mqh" input int startIndex_ = 0; // Starting index input int totalStrategies_ = 1; // Number of strategies input double depoPart_ = 1.0; // Part of the deposit for one strategy input ulong magic_ = 27182; // Magic CAdvisor *expert; // EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Check if the parameters are correct if(startIndex_ < 0 || startIndex_ + totalStrategies_ > 9) { return INIT_PARAMETERS_INCORRECT; } // Create and fill the array of strategy instances CStrategy *strategies[9]; strategies[0] = new CSimpleVolumesMarketStrategy( magic_ + 0, "EURGBP", PERIOD_H1, NormalizeDouble(0.01 / 0.16 * depoPart_, 2), 13, 0.3, 1.0, 0, 10500, 465, 1000, 3); strategies[1] = new CSimpleVolumesMarketStrategy( magic_ + 1, "EURGBP", PERIOD_H1, NormalizeDouble(0.01 / 0.09 * depoPart_, 2), 17, 1.7, 0.5, 0, 16500, 220, 1000, 3); strategies[2] = new CSimpleVolumesMarketStrategy( magic_ + 2, "EURGBP", PERIOD_H1, NormalizeDouble(0.01 / 0.16 * depoPart_, 2), 51, 0.5, 1.1, 0, 19500, 370, 22000, 3); strategies[3] = new CSimpleVolumesMarketStrategy( magic_ + 3, "GBPUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.25 * depoPart_, 2), 80, 1.1, 0.2, 0, 6000, 1190, 1000, 3); strategies[4] = new CSimpleVolumesMarketStrategy( magic_ + 4, "GBPUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.09 * depoPart_, 2), 128, 2.0, 0.9, 0, 2000, 1170, 1000, 3); strategies[5] = new CSimpleVolumesMarketStrategy( magic_ + 5, "GBPUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.14 * depoPart_, 2), 13, 1.5, 0.8, 0, 2500, 1375, 1000, 3); strategies[6] = new CSimpleVolumesMarketStrategy( magic_ + 6, "EURUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.23 * depoPart_, 2), 24, 0.1, 0.3, 0, 7500, 2400, 24000, 3); strategies[7] = new CSimpleVolumesMarketStrategy( magic_ + 7, "EURUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.20 * depoPart_, 2), 18, 0.2, 0.4, 0, 19500, 1480, 6000, 3); strategies[8] = new CSimpleVolumesMarketStrategy( magic_ + 8, "EURUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.22 * depoPart_, 2), 128, 0.7, 0.3, 0, 3000, 170, 42000, 3); expert = new CAdvisor(); // Add the necessary strategies to the EA for(int i = startIndex_; i < startIndex_ + totalStrategies_; i++) { expert.Add(strategies[i]); } return(INIT_SUCCEEDED); } void OnTick() { expert.Tick(); } void OnDeinit(const int reason) { delete expert; }
Auf diese Weise können wir die Optimierung des Anfangsindexes der Strategien im Strategie-Array nutzen, um Ergebnisse für jede Strategieinstanz zu erhalten. Starten wir ihn mit einer Ersteinlage von 100.000 USD und erhalten die folgenden Ergebnisse.
Abb. 2. Ergebnisse der Einzelläufe von neun Strategieinstanzen
Es ist klar, dass der Drawdown etwa 1 % der Ersteinlage beträgt, d. h. etwa 1.000 USD, wie wir bei der Auswahl der optimalen Größe der zu eröffnenden Positionen geplant haben. Die durchschnittliche Sharpe Ratio beträgt 1,3.
Schalten wir nun alle Instanzen ein und wählen wir den entsprechenden Multiplikator depoPart_, um einen Drawdown von 1000 USD zu erhalten. Wenn depoPart_ = 0,38 ist, bleibt der Drawdown innerhalb akzeptabler Grenzen.
Abb. 3. Ergebnisse der Prüfung des gleichzeitigen Betriebs von neun Strategieinstanzen
Vergleicht man die Ergebnisse der Arbeit einzelner Exemplare von Strategien und die Ergebnisse der gleichzeitigen Arbeit aller Exemplare, so kann man feststellen, dass wir bei gleichem Drawdown eine Steigerung des Gewinns um etwa das Dreifache sowie eine Erhöhung der Sharpe Ratio von 1,3 auf 2,84 erhalten haben.
Konzentrieren wir uns nun auf die Hauptaufgabe.
Klasse der virtuellen Positionen (Aufträge)
Erstellen wir also die versprochene Klasse CVirtualOrder und fügen wir ihr Felder hinzu, um alle Eigenschaften der offenen Positionen zu speichern.
class CVirtualOrder { private: //--- Order (position) properties ulong m_id; // Unique ID string m_symbol; // Symbol double m_lot; // Volume ENUM_ORDER_TYPE m_type; // Type double m_openPrice; // Open price double m_stopLoss; // StopLoss level double m_takeProfit; // TakeProfit level string m_comment; // Comment datetime m_openTime; // Open time //--- Closed order (position) properties double m_closePrice; // Close price datetime m_closeTime; // Close time string m_closeReason; // Closure reason double m_point; // Point value bool m_isStopLoss; // StopLoss activation property bool m_isTakeProfit;// TakeProfit activation property };
Jede virtuelle Position sollte eine eindeutige ID haben. Fügen wir also die statische Klassenvariable s_count hinzu, um die Anzahl aller im Programm erstellten Positionsobjekte zu zählen. Wenn ein neues Positionsobjekt erstellt wird, wird dieser Zähler um 1 erhöht und dieser Wert wird zu einer eindeutigen Positionsnummer. Wir setzen den Anfangswert von s_count auf 0.
Wir benötigen auch das Objekt der Klasse CSymbolInfo für Preisinformationen. Wir machen es auch zu einem statischen Mitglied der Klasse.
class CVirtualOrder { private: static int s_count; static CSymbolInfo s_symbolInfo; //--- Order (position) properties ... }; int CVirtualOrder::s_count = 0; CSymbolInfo CVirtualOrder::s_symbolInfo;
Es ist erwähnenswert, dass das Erstellen eines virtuellen Positionsobjekts und das „Öffnen“ einer virtuellen Position unterschiedliche Vorgänge sind. Das Positionsobjekt kann im Voraus erstellt werden und auf den Moment warten, in dem die Strategie eine virtuelle Position eröffnen möchte. In diesem Moment werden die Positionseigenschaften mit den aktuellen Werten von Symbol, Volumen, Eröffnungskurs und anderen gefüllt. Wenn die Strategie beschließt, eine Position zu schließen, speichert das Objekt die Werte für das Schließen: Preis, Zeit und Grund des Schließens. Beim nächsten Vorgang der Eröffnung einer virtuellen Position können wir dieselbe Instanz des Objekts verwenden, indem wir seine Schließeigenschaften löschen und es erneut mit neuen Werten für Symbol, Volumen, Eröffnungskurs und andere füllen.
Fügen wir Methoden zu dieser Klasse hinzu. Wir benötigen öffentliche Methoden zum Öffnen und Schließen einer virtuellen Position und einen Konstruktor. Nützlich sind auch die Methoden, die den Status der Position (ist sie offen und in welche Richtung?) und ihre wichtigsten Eigenschaften - Volumen und aktueller Gewinn - überprüfen.
class CVirtualOrder { //--- Previous code... public: CVirtualOrder(); // Constructor //--- Methods for checking the order (position) status bool IsOpen(); // Is the order open? bool IsMarketOrder(); // Is this a market position? bool IsBuyOrder(); // Is this an open BUY position? bool IsSellOrder(); // Is this an open SELL position? //--- Methods for obtaining order (position) properties double Volume(); // Volume with direction double Profit(); // Current profit //--- Methods for handling orders (positions) bool Open(string symbol, ENUM_ORDER_TYPE type, double lot, double sl = 0, double tp = 0, string comment = "", bool inPoints = true); // Opening an order (position) bool Close(); // Closing an order (position) };
Die Implementierung einiger dieser Methoden ist sehr einfach und kurz, sodass sie z. B. direkt in der Klassendeklaration platziert werden kann:
class CVirtualOrder : public CObject { // ... //--- Methods for checking the order (position) status bool IsOpen() { // Is the order open? return(this.m_openTime > 0 && this.m_closeTime == 0); }; bool IsMarketOrder() { // Is this a market position? return IsOpen() && (m_type == ORDER_TYPE_BUY || m_type == ORDER_TYPE_SELL); } // ... };
Der Konstruktor weist allen Eigenschaften der virtuellen Position mit Ausnahme einer - einer eindeutigen ID - über die Initialisierungsliste leere (im Sinne von offensichtlich ungültigen) Werte zu. Wie bereits erwähnt, wird der ID im Konstruktor ein Wert zugewiesen, der sich aus dem Wert des Zählers für zuvor erstellte Klassenobjekte ergibt. Dieser Wert bleibt während der gesamten EA-Operation erhalten. Vor der Zuweisung wird der Zähler der erstellten Objekte erhöht.
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualOrder::CVirtualOrder() : // Initialization list m_id(++s_count), // New ID = object counter + 1 m_symbol(""), m_lot(0), m_type(-1), m_openPrice(0), m_stopLoss(0), m_takeProfit(0), m_openTime(0), m_comment(""), m_closePrice(0), m_closeTime(0), m_closeReason(""), m_point(0) { }
Bevor wir die Methoden der Klasse CVirtualOrder weiter implementieren, sollten wir einen Blick in die Zukunft werfen und überlegen, wie wir die Klasse verwenden werden. Wir haben Strategieobjekte, die jetzt echte Marktpositionen eröffnen. Außerdem kennen wir die maximale Anzahl der offenen Marktpositionen (die in den Strategieparametern festgelegt ist). Wir wollen zu virtuellen Positionen übergehen. Dann wird auch ihre Zahl begrenzt sein. Das bedeutet, dass wir in einer Strategie ein Array von virtuellen Positionsobjekten erstellen können, das wir bei der Initialisierung der Strategie mit der erforderlichen Anzahl von virtuellen Positionen füllen und dann nur mit diesem Array arbeiten.
Wenn die Bedingungen für die Eröffnung einer neuen Position eintreten, nehmen wir eine virtuelle Position, die noch nicht eröffnet wurde, aus dem Array und machen sie zu einer offenen Position. Wenn die Bedingungen für eine Zwangsschließung von Positionen eintreten, wandeln wir diese in geschlossene Positionen um.
Solange es offene virtuelle Positionen gibt, muss jede Strategie diese Objekte bei jedem Tick auf die gleiche Weise verarbeiten: Sie geht alles der Reihe nach durch, prüft, ob das StopLoss- oder TakeProfit-Niveau erreicht wurde, und wenn ja, schließt sie die Position. Diese Gleichheit ermöglicht es uns, die Implementierung der Behandlung offener virtueller Verhaltenspositionen in ihre eigene Klasse zu übertragen und nur die entsprechende Methode aus der Strategie aufzurufen.
Die Klasse CVirtualOrder erhält die Methode Tick(), die die Bedingungen für das Schließen einer Position prüft, und wenn diese erfüllt sind, wird die Position in den geschlossenen Zustand überführt. Wenn sich der Zustand der Position ändert, gibt die Methode true zurück.
Fügen wir auch die statische Methode Tick() hinzu, die mehrere virtuelle Positionsobjekte gleichzeitig verarbeitet. Sie akzeptiert einen Link zu einem Array solcher Objekte als Parameter. Die Methode Tick() wird für jedes Array-Objekt aufgerufen. Wenn mindestens eine virtuelle Position geschlossen ist, wird schließlich „true“ zurückgegeben.
class CVirtualOrder { private: //... public: //... //--- Methods for handling orders (positions) bool Open(string symbol, ENUM_ORDER_TYPE type, double lot, double sl = 0, double tp = 0, string comment = "", bool inPoints = false ); // Open order (position) bool Tick(); // Handle tick for an order (position) bool Close(); // Close an order (position) static bool Tick(CVirtualOrder &orders[]); // Handle a tick for the array of virtual orders }; //... //+------------------------------------------------------------------+ //| Handle a tick of a single virtual order (position) | //+------------------------------------------------------------------+ bool CVirtualOrder::Tick() { if(IsMarketOrder()) { // If this is a market virtual position if(CheckClose()) { // Check if SL or TP levels have been reached Close(); // Close when reached return true; // Return the fact that there are changes in open positions } } return false; } //+------------------------------------------------------------------+ //| Handle a tick for the array of virtual orders (positions) | //+------------------------------------------------------------------+ bool CVirtualOrder::Tick(CVirtualOrder &orders[]) { bool isChanged = false; // We assume that there will be no changes for(int i = 0; i < ArraySize(orders); i++) { // For all orders (positions) isChanged |= orders[i].Tick(); // Check and close if necessary } return isChanged; } //+------------------------------------------------------------------+
Speichern wir diesen Code in der Datei VirtualOrder.mqh des aktuellen Ordners.
Verbesserung der einfachen Handelsstrategieklasse
Nun können wir zur Handelsstrategieklasse zurückkehren und Änderungen vornehmen, die es uns ermöglichen, mit virtuellen Positionen zu arbeiten. Wie wir bereits vereinbart haben, verfügen wir in der Basisklasse CStrategy bereits über das Array m_orders[] zur Speicherung virtueller Positionsobjekte. Daher ist sie auch in der Klasse CSimpleVolumesStrategy verfügbar. Die betreffende Strategie hat den Parameter m_maxCountOfOrders, der die maximale Anzahl der gleichzeitig offenen Positionen festlegt. Dann setzen wir die Größe des Arrays der virtuellen Positionen im Konstruktor gleich diesem Parameter.
Als Nächstes müssen wir nur die Eröffnung von realen Positionen in den Methoden OpenBuyOrder() und OpenSellOrder() durch die Eröffnung von virtuellen Positionen ersetzen. Es gibt derzeit nichts, womit wir die Eröffnung echter schwebender Aufträge ersetzen könnten, also werden wir diese Vorgänge einfach auskommentieren.
//+------------------------------------------------------------------+ //| Open BUY order | //+------------------------------------------------------------------+ void CSimpleVolumesStrategy::OpenBuyOrder() { // ... if(m_openDistance > 0) { /* // Set BUY STOP pending order res = trade.BuyStop(lot, ...); */ } else if(m_openDistance < 0) { /* // Set BUY LIMIT pending order res = trade.BuyLimit(lot, ...); */ } else { // Open a virtual BUY position for(int i = 0; i < m_maxCountOfOrders; i++) { // Iterate through all virtual positions if(!m_orders[i].IsOpen()) { // If we find one that is not open, then open it res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY, m_fixedLot, NormalizeDouble(sl, digits), NormalizeDouble(tp, digits)); break; // and exit } } } ... } //+------------------------------------------------------------------+ //| Open SELL order | //+------------------------------------------------------------------+ void CSimpleVolumesStrategy::OpenSellOrder() { // ... if(m_openDistance > 0) { /* // Set SELL STOP pending order res = trade.SellStop(lot, ...); */ } else if(m_openDistance < 0) { /* // Set SELL LIMIT pending order res = trade.SellLimit(lot, ...); */ } else { // Open a virtual SELL position for(int i = 0; i < m_maxCountOfOrders; i++) { // Iterate through all virtual positions if(!m_orders[i].IsOpen()) { // If we find one that is not open, then open it res = m_orders[i].Open(m_symbol, ORDER_TYPE_SELL, m_fixedLot, NormalizeDouble(sl, digits), NormalizeDouble(tp, digits)); break; // and exit } } } ... }
Speichern wir die Änderungen in der Datei SimpleVolumesStrategy.mqh des aktuellen Ordners.
Erstellung einer Klasse zur Umwandlung von offenen Volumina in Marktpositionen
Wir haben bereits eine Basisklasse für Objekte zur Umwandlung von offenen Volumina in Marktpositionen erstellt, die bisher nichts anderes tut, als das Array der verwendeten Strategien zu füllen. Nun müssen wir eine abgeleitete Klasse schreiben, die eine spezifische Implementierung der Platzierung von Positionen auf dem Markt enthält. Lassen Sie uns die Klasse CVolumeReceiver erstellen. Wir müssen eine ganze Menge Code hinzufügen, um die Methode Correct() zu implementieren. Wir werden sie in mehrere geschützte Klassenmethoden aufteilen.
#include "Receiver.mqh" //+------------------------------------------------------------------+ //| Class for converting open volumes into market positions | //+------------------------------------------------------------------+ class CVolumeReceiver : public CReceiver { protected: bool m_isNetting; // Is this a netting account? string m_symbols[]; // Array of used symbols double m_minMargin; // Minimum margin for opening CPositionInfo m_position; CSymbolInfo m_symbolInfo; CTrade m_trade; // Filling the array of open market volumes by symbols void FillSymbolVolumes(double &oldVolumes[]); // Correction of open volumes using the array of volumes virtual bool Correct(double &symbolVolumes[]); // Volume correction for this symbol bool CorrectPosition(string symbol, double oldVolume, double diffVolume); // Auxiliary methods bool ClearOpen(string symbol, double diffVolume); bool AddBuy(string symbol, double volume); bool AddSell(string symbol, double volume); bool CloseBuyPartial(string symbol, double volume); bool CloseSellPartial(string symbol, double volume); bool CloseHedgingPartial(string symbol, double volume, ENUM_POSITION_TYPE type); bool CloseFull(string symbol = ""); bool FreeMarginCheck(string symbol, double volume, ENUM_ORDER_TYPE type); public: CVolumeReceiver(ulong p_magic, double p_minMargin = 100); // Constructor virtual void Add(CStrategy *strategy) override; // Add strategy virtual bool Correct() override; // Adjustment of open volumes };
Der allgemeine Algorithmus für die Methode der Korrektur des offenen Volumens lautet wie folgt:
- Gehen wir für jedes verwendete Symbol alle Strategien durch und berechnen Sie das gesamte offene Volumen für jedes verwendete Symbol. Das resultierende newVolumes-Array wird an die nächste überladene Methode Correct() übergeben
//+------------------------------------------------------------------+ //| Adjustment of open volumes | //+------------------------------------------------------------------+ bool CVolumeReceiver::Correct() { int symbolsTotal = ArraySize(m_symbols); double newVolumes[]; ArrayResize(newVolumes, symbolsTotal); ArrayInitialize(newVolumes, 0); for(int j = 0; j < symbolsTotal; j++) { // For each used symbol for(int i = 0; i < ArraySize(m_strategies); i++) { // Iterate through all strategies if(m_strategies[i].Symbol() == m_symbols[j]) { // If the strategy uses this symbol newVolumes[j] += m_strategies[i].Volume(); // Add its open volume } } } // Call correction of open volumes using the array of volumes return Correct(newVolumes); }
- Wir legen für jedes Symbol fest, wie stark das Volumen der offenen Positionen für das Symbol verändert werden soll. Ggf. rufen wir die Volumenkorrekturmethode für dieses Symbol auf:
//+------------------------------------------------------------------+ //| Adjusting open volumes using the array of volumes | //+------------------------------------------------------------------+ bool CVolumeReceiver::Correct(double &newVolumes[]) { // ... bool res = true; // For each symbol for(int j = 0; j < ArraySize(m_symbols); j++) { // ... // Define how much the volume of open positions for the symbol should be changed double oldVolume = oldVolumes[j]; double newVolume = newVolumes[j]; // ... double diffVolume = newVolume - oldVolume; // If there is a need to adjust the volume for a given symbol, then do that if(MathAbs(diffVolume) > 0.001) { res = res && CorrectPosition(m_symbols[j], oldVolume, diffVolume); } } return res; }
- Für ein Symbol bestimmen wir, welche Art von Handelsvorgang wir durchführen müssen (hinzufügen, schließen und wieder öffnen), basierend auf den Werten des vorherigen offenen Volumens und der gewünschten Änderung, und rufen die entsprechende Hilfsmethode auf:
//+------------------------------------------------------------------+ //| Adjust volume by the symbol | //+------------------------------------------------------------------+ bool CVolumeReceiver::CorrectPosition(string symbol, double oldVolume, double diffVolume) { bool res = false; // ... double volume = MathAbs(diffVolume); if(oldVolume > 0) { // Have BUY position if(diffVolume > 0) { // New BUY position res = AddBuy(symbol, volume); } else if(diffVolume < 0) { // New SELL position if(volume < oldVolume) { res = CloseBuyPartial(symbol, volume); } else { res = CloseFull(symbol); if(res && volume > oldVolume) { res = AddSell(symbol, volume - oldVolume); } } } } else if(oldVolume < 0) { // Have SELL position if(diffVolume < 0) { // New SELL position res = AddSell(symbol, volume); } else if(diffVolume > 0) { // New BUY position if(volume < -oldVolume) { res = CloseSellPartial(symbol, volume); } else { res = CloseFull(symbol); if(res && volume > -oldVolume) { res = AddBuy(symbol, volume + oldVolume); } } } } else { // No old position res = ClearOpen(symbol, diffVolume); } return res; }
Wir speichern den Code in der Datei VolumeReceiver.mqh des aktuellen Ordners.
EA mit einer Strategie und virtuellen Positionen
Erstellen wir einen EA, der eine Instanz einer Handelsstrategie mit virtuellen Positionen verwendet, basierend auf der Datei SimpleVolumesMarketExpertSingle.mq5. Wir müssen die erforderlichen Dateien einbinden, wenn wir den EA-Konstruktor aufrufen, ihm das neue Objekt der Klasse CVolumeReceiver übergeben und die Klasse der erstellten Strategie ersetzen.
#include "Advisor.mqh" #include "SimpleVolumesStrategy.mqh" #include "VolumeReceiver.mqh" // Input parameters... CAdvisor *expert; // Pointer to the EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { expert = new CAdvisor(new CVolumeReceiver(magic_)); expert.Add(new CSimpleVolumesStrategy( symbol_, timeframe_, fixedLot_, signalPeriod_, signalDeviation_, signaAddlDeviation_, openDistance_, stopLevel_, takeLevel_, ordersExpiration_, maxCountOfOrders_) ); // Add one strategy instance return(INIT_SUCCEEDED); } void OnTick() { expert.Tick(); } void OnDeinit(const int reason) { delete expert; }
Wir speichern diesen Code in der Datei SimpleVolumesExpertSingle.mq5 im aktuellen Ordner.
Vergleich zwischen realem und virtuellem Handel
Starten wir EAs mit der gleichen Strategie über einen kurzen Zeitraum, mit den gleichen Strategieparametern, aber unterschiedlichen Arten der Eröffnung realer Positionen - direkt und über virtuelle Positionen. Speichern wir diese Ergebnisse in Berichten und sehen uns die Liste der von beiden EAs getätigten Geschäfte an.
Abb. 4. Von den beiden EAs ausgeführten Handel (mit und ohne virtuelle Positionen)
Um die Breite zu verringern, wurden Spalten, die in allen Zeilen die gleichen Werte aufweisen, wie das Symbol (immer EURGBP), das Volumen (immer 0,01) und andere, aus den Tabellen entfernt. Wie wir sehen, werden die ersten Positionen in beiden Fällen zum gleichen Preis und zu den gleichen Zeitpunkten eröffnet. Wenn wir eine offene Position SELL haben (2018.03.02 15:46:47 sell in) und eine BUY öffnen (2018.03.06 13:56:04 buy in), schließt der EA, der über virtuelle Positionen arbeitet, einfach die vorherige Position SELL (2018.03.06 13:56:04 buy out). Das Gesamtergebnis verbesserte sich dadurch, da der erste EA weiterhin Swaps für offene Positionen in verschiedenen Richtungen zahlte, während dies beim zweiten EA nicht der Fall war.
EA mit mehreren Strategien und virtuellen Positionen
Lassen Sie uns ähnliche Manipulationen mit dem EA aus der Datei SimpleVolumesMarketExpert.mq5 durchführen. Wir werden die erforderlichen Dateien beifügen. Wenn wir den EA-Konstruktor aufrufen, übergeben wir ihm das neue Objekt der Klasse CVolumeReceiver und ersetzen die Klasse der erstellten Strategien. Speichern Sie das Ergebnis in der Datei SimpleVolumesExpert.mq5 und sehen Sie sich die Ergebnisse an.
Abb. 5. Ergebnisse der EA-Arbeit mit neun Strategieinstanzen und virtuellen Positionen
Vergleicht man diese Ergebnisse mit denen eines ähnlichen EA, der keine virtuellen Positionen verwendet, kann man bei einigen Indikatoren eine Verbesserung feststellen: Der Gewinn hat sich leicht erhöht und der Drawdown hat sich verringert, die Sharpe Ratio und der Gewinnfaktor haben sich ebenfalls erhöht.
Schlussfolgerung
Wir haben einen weiteren Schritt zur Erreichung unseres Ziels gemacht. Durch die Umstellung auf virtuelle Positionsstrategien haben wir die Möglichkeit verbessert, dass eine große Anzahl von Handelsstrategien zusammenarbeiten kann, ohne sich gegenseitig zu behindern. Dies ermöglicht es uns auch, eine niedrigere Mindesteinlage für den Handel zu verwenden, als wenn wir jede Instanz der Strategie unabhängig voneinander handeln würden. Ein weiterer netter Bonus wird die Möglichkeit sein, an Netting-Konten zu arbeiten.
Aber es sind noch viele weitere Schritte zu unternehmen. So wurden bisher nur Strategien implementiert, die Marktpositionen eröffnen, aber keine schwebenden Aufträge. Auch Fragen der Geldverwaltung werden für die Zukunft aufgeschoben. Zurzeit handeln wir mit einem festen Volumen und wählen die optimale Positionsgröße manuell aus. Strategien, die auf mehreren Symbolen gleichzeitig arbeiten sollen (die nicht in einfachere Einzelsymbolstrategien aufgeteilt werden können), können diese Operationsstruktur nicht verwenden.
Bleiben Sie dran für weitere Updates.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/14107
![Wie Sie mit der Erfüllung von Händleraufträgen im Freelance-Service Geld verdienen können](https://c.mql5.com/2/80/How-to--MQL5-Freelance-avatar.png)
![Algorithmen zur Optimierung mit Populationen: Evolution sozialer Gruppen (ESG)](https://c.mql5.com/2/68/Population_optimization_algorithms_Evolution_of_Social_Groups_rESGw___LOGO.png)
![Algorithmen zur Optimierung mit Populationen: Künstliche multisoziale Suchobjekte (MSO)](https://c.mql5.com/2/69/Population_optimization_algorithms___Artificial_Multi-Social_Search_Objects_dMSOb____LOGO.png)
![Entwicklung eines Replay Systems (Teil 38): Den Weg ebnen (II)](https://c.mql5.com/2/61/Replay_Parte_38_Pavimentando_o_Terreno_LOGO.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.