Entwicklung eines Expertenberaters für mehrere Währungen (Teil 1): Zusammenarbeit von mehreren Handelsstrategien
Während meiner beruflichen Tätigkeit hatte ich mit verschiedenen Handelsstrategien zu tun. In der Regel setzen EAs nur eine Handelsidee um. Die Schwierigkeiten, eine stabile Zusammenarbeit vieler EAs auf einem Terminal zu gewährleisten, zwingen uns normalerweise dazu, nur eine kleine Anzahl der besten EAs auszuwählen. Dennoch ist es schade, wenn man aus diesem Grund völlig brauchbare Strategien verwirft. Wie können wir dafür sorgen, dass sie zusammenarbeiten?
Definition des Problems
Wir müssen entscheiden, was wir wollen und was wir haben.
Das haben wir (oder haben wir fast):
- einige verschiedene Handelsstrategien, die auf verschiedenen Symbolen und Zeitrahmen funktionieren, in Form eines vorgefertigten EA-Codes oder eines formulierten Regelsatzes für die Durchführung von Handelsoperationen,
- ein Startguthaben und
- ein maximal zulässiges Drawdown.
Wir wollen:
- eine Zusammenarbeit aller ausgewählten Strategien auf einem Konto für mehrere Symbole und Zeitrahmen,
- eine Aufteilung des Startguthabens auf alle zu gleichen Teilen oder gemäß den festgelegten Verhältnissen,
- die automatische Berechnung der Volumina der eröffneten Positionen zur Einhaltung der maximal zulässigen Inanspruchnahme,
- eine korrekte Handhabung von Terminal-Neustarts und
- die Fähigkeit, in MetaTrader 5 und 4 zu laufen.
Wir werden einen objektorientierten Ansatz, MQL5 und einen Standard-Tester in MetaTrader 5 verwenden.
Die Aufgabe ist ziemlich umfangreich, deshalb werden wir sie Schritt für Schritt lösen.
Nehmen wir in der ersten Phase eine einfache Handelsidee. Lassen Sie uns damit einen einfachen EA erstellen. Optimieren Sie es und wählen Sie die beiden besten Parametersätze aus. Erstellen Sie einen EA, der zwei Kopien des ursprünglichen einfachen EA enthält, und betrachten Sie seine Ergebnisse.
Von der Handelsidee zur Strategie
Betrachten wir folgende Idee als ein Experiment.
Nehmen wir an, dass sich der Kurs eines bestimmten Symbols bei intensivem Handel pro Zeiteinheit stärker verändern kann als bei schleppendem Handel mit einem Symbol. Wenn wir dann sehen, dass sich der Handel intensiviert hat und der Preis sich in eine bestimmte Richtung verändert hat, dann wird er sich vielleicht in naher Zukunft in dieselbe Richtung verändern. Lassen Sie uns versuchen, daraus Profit zu schlagen.
Eine Handelsstrategie ist eine Reihe von Regeln für das Öffnen und Schließen von Positionen auf der Grundlage einer Handelsidee. Sie enthält keine unbekannten Parameter. Mit diesem Regelwerk sollte es möglich sein, zu jedem Zeitpunkt, an dem die Strategie läuft, zu bestimmen, ob und welche Positionen eröffnet werden sollen.
Versuchen wir, die Idee in eine Strategie zu verwandeln. Zunächst einmal müssen wir irgendwie eine Zunahme der Handelsintensität feststellen. Ohne diese Angaben können wir nicht feststellen, wann wir Positionen eröffnen sollten. Hierfür verwenden wir das Tick-Volumen, d.h. die Anzahl der neuen Kurse, die während der aktuellen Kerze am Terminal eingegangen sind. Ein größeres Tick-Volumen wird als Zeichen für einen intensiveren Handel angesehen. Bei verschiedenen Symbolen kann die Intensität jedoch sehr unterschiedlich sein. Daher können wir nicht ein einziges Niveau für ein Tick-Volumen festlegen, dessen Überschreitung wir als Beginn eines intensiven Handels betrachten. Um dieses Niveau zu bestimmen, können wir von dem durchschnittlichen Tick-Volumen mehrerer Kerzen ausgehen. Nach einigem Nachdenken können wir die folgende Beschreibung geben:
Wir platzieren einen schwebenden Auftrag (pending order) in dem Moment, in dem das Tick-Volumen der Kerze das durchschnittliche Volumen in Richtung der aktuellen Kerze übersteigt. Jeder Auftrag hat eine Verfallszeit, nach der sie gelöscht wird. Wenn ein schwebender Auftrag zu einer Position geworden ist, wird er erst bei Erreichen der angegebenen StopLoss- und TakeProfit-Werte geschlossen. Wenn das Tick-Volumen den Durchschnitt noch weiter übersteigt, können zusätzlich zu den bereits eröffneten schwebenden Aufträgen weitere Aufträge erteilt werden.
Dies ist eine ausführlichere, aber nicht vollständige Beschreibung. Deshalb lesen wir ihn noch einmal und markieren alle Stellen, an denen etwas unklar ist. Hier sind ausführlichere Erläuterungen erforderlich.
Hier sind die Fragen, die sich gestellt haben:
- „Platziere einen schwebenden Auftrag..“ — Welche schwebenden Aufträge sollten wir platzieren?
- „... durchschnittliches Volumen...“ — Wie berechnet man das durchschnittliche Volumen einer Kerze?
- „... übersteigt die durchschnittliche Menge... “ — Wie wird eine Überschreitung der durchschnittlichen Menge festgestellt?
- „... Wenn das Tick-Volumen den Durchschnitt noch mehr übersteigt ...“ — Wie kann man diese größere Überschreitung bestimmen?
- „... zusätzliche Aufträge können erteilt werden “ — Wie viele Aufträge können insgesamt erteilt werden?
Welche schwebenden Aufträge werden wir erteilen? Ausgehend von dieser Idee hoffen wir, dass sich der Kurs weiterhin in dieselbe Richtung bewegt, in die er sich seit Beginn der Kerze bewegt hat. Liegt der Kurs beispielsweise aktuell höher als zu Beginn der Kerzenperiode, sollten wir einen schwebenden Kaufauftrag eröffnen. Wenn wir BUY_LIMIT eröffnen, sollte der Preis zunächst ein wenig zurückgehen (fallen), und dann sollte der Preis wieder steigen, damit die eröffnete Position einen Gewinn erzielt. Wenn wir BUY_STOP eröffnen, sollte der Kurs noch ein wenig weiter steigen, um eine Position zu eröffnen, und dann noch höher klettern, um einen Gewinn zu erzielen.
Es ist nicht sofort klar, welche dieser Optionen besser ist. Der Einfachheit halber öffnen wir daher immer Stop-Orders (BUY_STOP und SELL_STOP). In Zukunft kann dies zu einem Strategieparameter gemacht werden, dessen Wert bestimmt, welche Aufträge geöffnet werden.
Wie berechnet man das durchschnittliche Kerzenvolumen? Zur Berechnung des Durchschnittsvolumens müssen die Kerzen ausgewählt werden, deren Volumen in die Durchschnittsberechnung einfließen soll. Nehmen wir eine Reihe von aufeinanderfolgenden, zuletzt geschlossenen Kerzen. Wenn wir dann die Anzahl der Kerzen festlegen, können wir das durchschnittliche Tick-Volumen berechnen.
Wie wird die Überschreitung des durchschnittlichen Volumens bestimmt? Wenn wir die Bedingung
V > V_avr,
mit
V ist ein Tick-Volumen der aktuellen Kerze und
V_avr ist ein durchschnittliches Tick-Volumen,
dann wird diese Bedingung bei etwa der Hälfte der Kerzen erfüllt sein. Ausgehend von dieser Idee sollten wir nur dann Aufträge erteilen, wenn das Volumen den Durchschnitt deutlich übersteigt. Ansonsten kann dies im Gegensatz zu früheren Kerzen noch nicht als Anzeichen für einen intensiveren Handel bei dieser Kerze gewertet werden. Zum Beispiel können wir die folgende Gleichung verwenden:
V > V_avr + D * V_avr,
wobei D ein numerisches Verhältnis ist. Wenn D = 1 ist, erfolgt die Eröffnung, wenn das aktuelle Volumen den Durchschnitt um das Zweifache übersteigt, und wenn z. B. D = 2 ist, erfolgt die Eröffnung, wenn das aktuelle Volumen den Durchschnitt um das Dreifache übersteigt.
Diese Bedingung kann jedoch nur für die Eröffnung eines Auftrags angewendet werden, denn wenn sie für die Eröffnung des zweiten und der folgenden Aufträge verwendet wird, werden diese unmittelbar nach dem ersten Auftrag eröffnet. Dies kann einfach durch die Eröffnung einer Bestellung mit einem größeren Volumen ersetzt werden.
Wie wird der größere Überschuss ermittelt? Zu diesem Zweck fügen wir der Bedingungsgleichung einen weiteren Parameter hinzu - die Anzahl der offenen Aufträge N:
V > V_avr + D * V_avr + N * D * V_avr.
Damit der zweite Auftrag nach dem ersten eröffnet werden kann (d. h. N = 1), muss die folgende Bedingung erfüllt sein:
V > V_avr + 2 * D * V_avr.
Um die erste Ordnung (N = 0) zu eröffnen, nimmt die Gleichung die uns bereits bekannte Form an:
V > V_avr + D * V_avr.
Schließlich die letzte Korrektur der Eingangsgleichung. Wir machen zwei unabhängige Parameter D und D_add für die erste und die folgende Bestellung anstelle des gleichen D:
V > V_avr + D * V_avr + N * D_add * V_avr,
V > V_avr * (1 + D + N * D_add)
Es scheint, dass wir dadurch eine größere Freiheit bei der Auswahl der optimalen Parameter für die Strategie erhalten.
Wenn unsere Bedingung den Wert N als Gesamtzahl der Aufträge und Positionen verwendet, dann bedeutet dies, dass jeder schwebenden Auftrag zu einer separaten Position wird und das Volumen einer bereits offenen Position nicht erhöht. Daher müssen wir den Anwendungsbereich einer solchen Strategie vorerst auf Konten beschränken, bei denen die Positionen unabhängig voneinander verbucht werden („Hedging“).
Wenn alles klar ist, listen wir die Variablen auf, die verschiedene Werte und nicht nur einen einzigen Wert annehmen können. Dies werden unsere strategischen Eingabeparameter sein. Berücksichtigen wir, dass wir für die Eröffnung von Aufträgen auch das Volumen, den Abstand zum aktuellen Kurs, die Verfallszeit und die StopLoss- und TakeProfit-Niveaus kennen müssen. Dann erhalten wir die folgende Beschreibung:
Der EA läuft auf einem bestimmten Symbol und Periode (Zeitrahmen) auf dem Hedge-Konto
Die Eingabeparameter:
- Anzahl der Kerzen für die Volumenmittelung (K)
- Relative Abweichung vom Durchschnitt für die Eröffnung der ersten Bestellung (D)
- Relative Abweichung vom Durchschnitt für die Eröffnung des zweiten und der folgenden Aufträge (D_add)
- Abstand des schwebenden Auftrags zum Preis
- Stop-Loss (in Punkten)
- Take-Profit (in Punkten)
- Verfallszeit der schwebenden Aufträge (in Minuten)
- Maximale Anzahl von gleichzeitig offenen Aufträgen (N_max)
- Einzelnes Auftragsvolumen
Ermittlung der Anzahl der offenen Aufträge und Positionen (N).
Wenn N kleiner als N_max ist, dann:
Ermitteln wir das durchschnittliche Tick-Volumen für die letzten K geschlossenen Kerzen V_avr.
Wenn die Bedingung V > V_avr * (1 + D + N * D_add) erfüllt ist, dann:
Bestimmen wir die Richtung der Preisänderung der aktuellen Kerze: Steigt der Preis, werden wir einen BUY_STOP platzieren, sonst einen SELL_STOP,
eine schwebende Order mit dem in den Parametern des angegebenen Abstands, der Verfallszeit und StopLoss und TakeProfit.
Umsetzung einer Handelsstrategie
Beginnen wir mit dem Schreiben des Codes. Zunächst listen wir alle Parameter auf, wobei wir sie der Übersichtlichkeit halber in Gruppen unterteilen und jeden Parameter mit einem Kommentar versehen. Diese Kommentare (falls vorhanden) werden beim Start des EA im Parameterdialog und auf der Registerkarte Parameter im Strategietester anstelle der von uns gewählten Variablennamen angezeigt.
Für den Moment legen wir nur einige Standardwerte fest. Wir werden bei der Optimierung nach den besten suchen.
input group "=== Opening signal parameters" input int signalPeriod_ = 48; // Number of candles for volume averaging input double signalDeviation_ = 1.0; // Relative deviation from the average to open the first order input double signaAddlDeviation_ = 1.0; // Relative deviation from the average for opening the second and subsequent orders input group "=== Pending order parameters" input int openDistance_ = 200; // Distance from price to pending order input double stopLevel_ = 2000; // Stop Loss (in points) input double takeLevel_ = 75; // 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 magicN_ = 27181; // Magic
Da der EA Handelsoperationen durchführen wird, erstellen wir ein globales Objekt der Klasse CTrade. Wir platzieren schwebende Aufträge, indem wir die Objektmethoden aufrufen.
CTrade trade; // Object for performing trading operations
Beachten Sie, dass globale Variablen (oder Objekte) Variablen (oder Objekte) sind, die außerhalb einer Funktion im EA-Code deklariert sind. Dadurch sind sie in allen unseren EA-Funktionen verfügbar. Sie dürfen nicht mit globalen Terminalvariablen verwechselt werden.
Um die Parameter für Eröffnungsaufträge zu berechnen, müssen wir die aktuellen Preise und andere Symboleigenschaften erhalten, mit denen der EA gestartet wird. Dazu erstellen wir ein globales Objekt der Klasse CSymbolInfo.
CSymbolInfo symbolInfo; // Object for obtaining data on the symbol properties
Außerdem müssen wir die Anzahl der offenen Aufträge und Positionen zählen. Zu diesem Zweck erstellen wir globale Objekte der Klassen COrderInfo und CPositionInfo, um Daten über offene Aufträge und Positionen zu erhalten. Wir werden die Menge selbst in zwei globalen Variablen speichern - countOrders und countPositions.
COrderInfo orderInfo; // Object for receiving information about placed orders CPositionInfo positionInfo; // Object for receiving information about open positions int countOrders; // Number of placed pending orders int countPositions; // Number of open positions
Um das durchschnittliche Tick-Volumen mehrerer Kerzen zu berechnen, können wir zum Beispiel den technischen Indikator iVolumes verwenden. Um seine Werte zu erhalten, benötigen wir eine Variable, die das Handle dieses Indikators speichert (eine ganze Zahl, die die Seriennummer dieses Indikators unter allen anderen im EA verwendeten speichert). Um das durchschnittliche Volumen zu ermitteln, müssen wir zunächst die Werte aus dem Indikatorpuffer in ein vorbereitendes Array kopieren. Wir werden auch dieses Array global machen.
int iVolumesHandle; // Tick volume indicator handle double volumes[]; // Receiver array of indicator values (volumes themselves)
Nun können wir mit der EA-Initialisierungsfunktion OnInit() und der Tick-Verarbeitungsfunktion OnTick() fortfahren.
Während der Initialisierung können wir Folgendes tun:
- Wir laden den Indikator, um die Tick-Volumina zu erhalten, und sichern uns sein Handle,
- wir legen die Größe des empfangenden Arrays entsprechend der Anzahl der Kerzen fest, um das durchschnittliche Volumen zu berechnen, und legen seine Adressierung wie in der Zeitreihe fest
- und schließlich die Magic-Nummer für die Platzierung von Aufträgen über das Handelsobjekt.
So wird unsere Initialisierungsfunktion aussehen:
int OnInit() { // Load the indicator to get tick volumes iVolumesHandle = iVolumes(Symbol(), PERIOD_CURRENT, VOLUME_TICK); // Set the size of the tick volume receiving array and the required addressing ArrayResize(volumes, signalPeriod_); ArraySetAsSeries(volumes, true); // Set Magic Number for placing orders via 'trade' trade.SetExpertMagicNumber(magicN_); return(INIT_SUCCEEDED); }
Gemäß der Strategiebeschreibung sollten wir damit beginnen, die Anzahl der offenen Aufträge und Positionen in der Tick-Verarbeitungsfunktion zu ermitteln. Lassen Sie uns dies als separate Funktion UpdateCounts() implementieren. In dieser Funktion werden wir alle offenen Positionen und Aufträge durchgehen und nur diejenigen zählen, deren Magic-Nummer mit der unseres EA übereinstimmt.
void UpdateCounts() { // Reset position and order counters countPositions = 0; countOrders = 0; // Loop through all positions for(int i = 0; i < PositionsTotal(); i++) { // If the position with index i is selected successfully and its Magic is ours, then we count it if(positionInfo.SelectByIndex(i) && positionInfo.Magic() == magicN_) { countPositions++; } } // Loop through all orders for(int i = 0; i < OrdersTotal(); i++) { // If the order with index i is selected successfully and its Magic is the one we need, then we consider it if(orderInfo.SelectByIndex(i) && orderInfo.Magic() == magicN_) { countOrders++; } } }
Anschließend vergewissern wir uns, dass die Anzahl der offenen Positionen und Aufträge die in den Einstellungen angegebene Anzahl nicht übersteigt. In diesem Fall müssen wir prüfen, ob die Bedingungen für die Eröffnung eines neuen Auftrags erfüllt sind. Wir sollten diese Prüfung als separate SignalForOpen()-Funktion implementieren. Sie gibt einen von drei möglichen Werten zurück:
- +1 — Signal zur Eröffnung eines BUY_STOP
- 0 — kein Signal
- -1 — Signal zur Eröffnung eines SELL_STOP
Um schwebende Aufträge zu platzieren, werden wir ebenfalls zwei separate Funktionen schreiben: OpenBuyOrder() und OpenSellOrder().
Jetzt können wir eine vollständige Implementierung der Funktion OnTick() schreiben.
void OnTick() { // Count open positions and orders UpdateCounts(); // If their number is less than allowed if(countOrders + countPositions < maxCountOfOrders_) { // Get an open signal int signal = SignalForOpen(); if(signal == 1) { // If there is a buy signal, then OpenBuyOrder(); // open the BUY_STOP order } else if(signal == -1) { // If there is a sell signal, then OpenSellOrder(); // open the SELL_STOP order } } }
Danach fügen wir die Implementierung der übrigen Funktionen hinzu und der EA-Code ist fertig. Speichern wir sie in der Datei SimpleVolumes.mq5 im aktuellen Ordner.
#include <Trade\OrderInfo.mqh> #include <Trade\PositionInfo.mqh> #include <Trade\SymbolInfo.mqh> #include <Trade\Trade.mqh> input group "=== Opening signal parameters" input int signalPeriod_ = 48; // Number of candles for volume averaging input double signalDeviation_ = 1.0; // Relative deviation from the average to open the first order input double signaAddlDeviation_ = 1.0; // Relative deviation from the average for opening the second and subsequent orders input group "=== Pending order parameters" input int openDistance_ = 200; // Distance from price to pending order input double stopLevel_ = 2000; // Stop Loss (in points) input double takeLevel_ = 75; // 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 magicN_ = 27181; // Magic CTrade trade; // Object for performing trading operations COrderInfo orderInfo; // Object for receiving information about placed orders CPositionInfo positionInfo; // Object for receiving information about open positions int countOrders; // Number of placed pending orders int countPositions; // Number of open positions CSymbolInfo symbolInfo; // Object for obtaining data on the symbol properties int iVolumesHandle; // Tick volume indicator handle double volumes[]; // Receiver array of indicator values (volumes themselves) //+------------------------------------------------------------------+ //| Initialization function of the expert | //+------------------------------------------------------------------+ int OnInit() { // Load the indicator to get tick volumes iVolumesHandle = iVolumes(Symbol(), PERIOD_CURRENT, VOLUME_TICK); // Set the size of the tick volume receiving array and the required addressing ArrayResize(volumes, signalPeriod_); ArraySetAsSeries(volumes, true); // Set Magic Number for placing orders via 'trade' trade.SetExpertMagicNumber(magicN_); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| "Tick" event handler function | //+------------------------------------------------------------------+ void OnTick() { // Count open positions and orders UpdateCounts(); // If their number is less than allowed if(countOrders + countPositions < maxCountOfOrders_) { // Get an open signal int signal = SignalForOpen(); if(signal == 1) { // If there is a buy signal, then OpenBuyOrder(); // open the BUY_STOP order } else if(signal == -1) { // If there is a sell signal, then OpenSellOrder(); // open the SELL_STOP order } } } //+------------------------------------------------------------------+ //| Calculate the number of open orders and positions | //+------------------------------------------------------------------+ void UpdateCounts() { // Reset position and order counters countPositions = 0; countOrders = 0; // Loop through all positions for(int i = 0; i < PositionsTotal(); i++) { // If the position with index i is selected successfully and its Magic is ours, then we count it if(positionInfo.SelectByIndex(i) && positionInfo.Magic() == magicN_) { countPositions++; } } // Loop through all orders for(int i = 0; i < OrdersTotal(); i++) { // If the order with index i is selected successfully and its Magic is the one we need, then we consider it if(orderInfo.SelectByIndex(i) && orderInfo.Magic() == magicN_) { countOrders++; } } } //+------------------------------------------------------------------+ //| Open the BUY_STOP order | //+------------------------------------------------------------------+ void OpenBuyOrder() { // Update symbol current price data symbolInfo.Name(Symbol()); symbolInfo.RefreshRates(); // Retrieve the necessary symbol and price data double point = symbolInfo.Point(); int digits = symbolInfo.Digits(); double bid = symbolInfo.Bid(); double ask = symbolInfo.Ask(); int spread = symbolInfo.Spread(); // Let's make sure that the opening distance is not less than the spread int distance = MathMax(openDistance_, spread); // Opening price double price = ask + distance * point; // StopLoss and TakeProfit levels double sl = NormalizeDouble(price - stopLevel_ * point, digits); double tp = NormalizeDouble(price + (takeLevel_ + spread) * point, digits); // Expiration time datetime expiration = TimeCurrent() + ordersExpiration_ * 60; // Order volume double lot = fixedLot_; // Set a pending order bool res = trade.BuyStop(lot, NormalizeDouble(price, digits), Symbol(), NormalizeDouble(sl, digits), NormalizeDouble(tp, digits), ORDER_TIME_SPECIFIED, expiration); if(!res) { Print("Error opening order"); } } //+------------------------------------------------------------------+ //| Open the SELL_STOP order | //+------------------------------------------------------------------+ void OpenSellOrder() { // Update symbol current price data symbolInfo.Name(Symbol()); symbolInfo.RefreshRates(); // Retrieve the necessary symbol and price data double point = symbolInfo.Point(); int digits = symbolInfo.Digits(); double bid = symbolInfo.Bid(); double ask = symbolInfo.Ask(); int spread = symbolInfo.Spread(); // Let's make sure that the opening distance is not less than the spread int distance = MathMax(openDistance_, spread); // Opening price double price = bid - distance * point; // StopLoss and TakeProfit levels double sl = NormalizeDouble(price + stopLevel_ * point, digits); double tp = NormalizeDouble(price - (takeLevel_ + spread) * point, digits); // Expiration time datetime expiration = TimeCurrent() + ordersExpiration_ * 60; // Order volume double lot = fixedLot_; // Set a pending order bool res = trade.SellStop(lot, NormalizeDouble(price, digits), Symbol(), NormalizeDouble(sl, digits), NormalizeDouble(tp, digits), ORDER_TIME_SPECIFIED, expiration); if(!res) { Print("Error opening order"); } } //+------------------------------------------------------------------+ //| Signal for opening pending orders | //+------------------------------------------------------------------+ int SignalForOpen() { // By default, there is no signal int signal = 0; // Copy volume values from the indicator buffer to the receiving array int res = CopyBuffer(iVolumesHandle, 0, 0, signalPeriod_, volumes); // If the required amount of numbers have been copied if(res == signalPeriod_) { // Calculate their average value double avrVolume = ArrayAverage(volumes); // If the current volume exceeds the specified level, then if(volumes[0] > avrVolume * (1 + signalDeviation_ + (countOrders + countPositions) * signaAddlDeviation_)) { // if the opening price of the candle is less than the current (closing) price, then if(iOpen(Symbol(), PERIOD_CURRENT, 0) < iClose(Symbol(), PERIOD_CURRENT, 0)) { signal = 1; // buy signal } else { signal = -1; // otherwise, sell signal } } } return signal; } //+------------------------------------------------------------------+ //| Number array average value | //+------------------------------------------------------------------+ double ArrayAverage(const double &array[]) { double s = 0; int total = ArraySize(array); for(int i = 0; i < total; i++) { s += array[i]; } return s / MathMax(1, total); } //+------------------------------------------------------------------+
Beginnen wir mit der Optimierung der EA-Parameter für EURGBP H1 auf MetaQuotes-Kursen vom 2018-01-01 bis 2023-01-01 mit einer Starteinlage von 100 000 USD und einem Mindestlot von 0,01. Beachten Sie, dass ein und derselbe EA leicht abweichende Ergebnisse zeigen kann, wenn er mit Kursen von verschiedenen Brokern getestet wird. Manchmal können diese Ergebnisse auch stark abweichen.
Wählen wir zwei schöne Parametersätze mit den folgenden Ergebnissen:
Abb. 1. Testergebnisse für [130, 0.9, 1.4, 231, 3750, 50, 600, 3, 0.01]
Abb. 2. Testergebnisse für [159, 1.7, 0.8, 248, 3600, 495, 39000, 3, 0.01]
Es war kein Zufall, dass der Test auf einer großen Ausgangslagerstätte durchgeführt wurde. Der Grund dafür ist, dass, wenn der EA Positionen mit einem festen Volumen eröffnet, der Lauf vorzeitig beendet werden kann, wenn der Drawdown größer wird als die verfügbaren Mittel. In diesem Fall wissen wir nicht, ob es möglich gewesen wäre, das Volumen der offenen Positionen vernünftigerweise zu reduzieren (oder entsprechend die Starteinlage zu erhöhen), um einen Verlust zu vermeiden, während dieselben Parameter verwendet werden.
Schauen wir uns ein Beispiel an. Nehmen wir an, unsere Starteinlage beträgt 1.000 USD. Bei der Ausführung im Tester erhielten wir folgende Ergebnisse:
- Die letzte Kontostand beträgt USD 11.000 (Gewinn 1.000%, der EA verdient USD +10.000 bei anfänglichen USD 1.000).
- Der maximale absolute Drawdown beträgt 2.000 USD.
Offensichtlich hatten wir einfach Glück, dass ein solcher Drawdown erst eintrat, nachdem der EA die Einlage auf über 2.000 USD erhöht hatte. Daher wurde der Test abgeschlossen und wir konnten diese Ergebnisse sehen. Wäre ein solcher Rückschlag früher eingetreten (z. B. wenn wir einen anderen Beginn des Testzeitraums gewählt hätten), hätten wir die gesamte Einlage verloren.
Wenn wir Testläufe manuell durchführen, können wir das Volumen in den Parametern ändern oder die Startkaution erhöhen und den Lauf erneut starten. Wenn jedoch während der Optimierung Läufe durchgeführt werden, ist dies nicht möglich. In diesem Fall kann ein potenziell guter Parametersatz aufgrund falsch gewählter Money-Management-Einstellungen abgelehnt werden. Um die Wahrscheinlichkeit eines solchen Ergebnisses zu verringern, können wir die Optimierung mit einer anfänglich sehr hohen Starteinlage und einem Mindestvolumen durchführen.
Um auf das Beispiel zurückzukommen: Wenn die Ersteinlage 100.000 USD beträgt, dann würde im Falle eines wiederholten Drawdowns von 2.000 USD nicht der Verlust der gesamten Einlage eintreten und der Tester würde diese Ergebnisse erhalten. Und wir könnten berechnen, dass, wenn der maximal zulässige Drawdown für uns 10 % beträgt, die Ersteinlage mindestens 20.000 $ betragen sollte. In diesem Fall beträgt die Rentabilität nur 50% (der EA verdiente + 10.000 USD zu den ursprünglichen 20.000 USD)
Führen wir ähnliche Berechnungen für unsere beiden ausgewählten Parameterkombinationen für die Starteinlage von 10.000 USD und den zulässigen Drawdown von 10 % der Starteinlage durch.
Parameter | Losgröße | Drawdown | Gewinn | Annehmbarer Drawdown | Annehmbare Losgröße | Annehmbarer Zugewinn |
---|---|---|---|---|---|---|
L | D | P | Da | La = L * (Da / D) | Pa = P * (Da / D) | |
[130, 0.9, 1.4, 231, 3750, 50, 600, 3, 0.01] | 0.01 | 28.70 (0.04%) | 260.41 | 1000 (10%) | 0.34 | 9073 (91%) |
[159, 1.7, 0.8, 248, 3600, 495, 39000, 3, 0.01] | 0.01 | 92.72 (0.09%) | 666.23 | 1000 (10%) | 0.10 | 7185 (72%) |
Wie wir sehen, können beide Eingabeoptionen annähernd ähnliche Renditen erzielen (~80%). Die erste Option bringt in absoluten Zahlen weniger Ertrag, allerdings mit einem geringeren Drawdown. Daher können wir in diesem Fall das Volumen der eröffneten Positionen stärker erhöhen als bei der zweiten Option, die zwar mehr einbringt, aber auch einen größeren Drawdown zulässt.
Wir haben also mehrere vielversprechende Eingabekombinationen gefunden. Fangen wir an, sie in einem EA zu kombinieren.
Basisstrategieklasse
Erstellen wir die Klasse CStrategy, in der wir alle Eigenschaften und Methoden aller Strategien sammeln werden. Zum Beispiel wird jede Strategie eine Art von Symbol und Zeitrahmen haben, unabhängig von ihrer Beziehung zu Indikatoren. Wir werden auch jeder Strategie eine eigene Magic-Nummer für die Eröffnung von Positionen und die Größe einer Position zuweisen. Der Einfachheit halber werden wir die Funktionsweise der Strategie mit einer variablen Positionsgröße vorerst nicht betrachten. Wir werden dies auf jeden Fall später tun.
Von den erforderlichen Methoden sind nur der Konstruktor, der die Strategieparameter initialisiert, die Initialisierungsmethode und der OnTick-Event-Handler zu erkennen. Wir erhalten den folgenden Code:
class CStrategy : public CObject { protected: ulong m_magic; // Magic string m_symbol; // Symbol (trading instrument) ENUM_TIMEFRAMES m_timeframe; // Chart period (timeframe) double m_fixedLot; // Size of opened positions (fixed) public: // Constructor CStrategy(ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot); virtual int Init() = 0; // Strategy initialization - handling OnInit events virtual void Tick() = 0; // Main method - handling OnTick events };
Die Methoden Init() und Tick() werden als rein virtuell deklariert (auf den Methodenkopf folgt = 0). Das bedeutet, dass wir die Implementierung dieser Methoden nicht in die Klasse CStrategy schreiben werden. Auf der Grundlage dieser Klasse werden wir abgeleitete Klassen erstellen, in denen die Methoden Init() und Tick() unbedingt vorhanden sein sollten und die die Implementierung spezifischer Handelsregeln enthalten.
Die Klassenbeschreibung ist fertig. Danach fügen wir die notwendige Implementierung des Konstruktors hinzu. Da es sich hierbei um eine Methodenfunktion handelt, die automatisch aufgerufen wird, wenn ein Strategieobjekt erstellt wird, müssen wir in dieser Methode sicherstellen, dass die Strategieparameter initialisiert werden. Dem Konstruktor werden vier Parameter übergeben und er weist deren Werte den entsprechenden Klassenvariablen über die Initialisierungsliste zu.
CStrategy::CStrategy( ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot) : // Initialization list m_magic(p_magic), m_symbol(p_symbol), m_timeframe(p_timeframe), m_fixedLot(p_fixedLot) {}
Wir speichern diesen Code in der Datei Strategy.mqh des aktuellen Ordners.
Die Klasse der Handelsstrategie
Übertragen wir die Logik des ursprünglichen einfachen EA auf eine neue abgeleitete Klasse CSimpleVolumesStrategy. Dazu machen wir alle Eingabevariablen und globalen Variablen zu Mitgliedern der Klasse. Wir werden die Variablen fixedLot_ und magicN_ durch die von der Basisklasse CStrategy geerbten Basisklassenmitglieder m_fixedLot und m_magic ersetzen.
#include "Strategy.mqh" class CSimpleVolumeStrategy : public CStrategy { //--- Open signal parameters int signalPeriod_; // Number of candles for volume averaging double signalDeviation_; // Relative deviation from the average to open the first order double signaAddlDeviation_; // Relative deviation from the average for opening the second and subsequent orders //--- Pending order parameters int openDistance_; // Distance from price to pending order double stopLevel_; // Stop Loss (in points) double takeLevel_; // Take Profit (in points) int ordersExpiration_; // Pending order expiration time (in minutes) //--- Money management parameters int maxCountOfOrders_; // Maximum number of simultaneously open orders CTrade trade; // Object for performing trading operations COrderInfo orderInfo; // Object for receiving information about placed orders CPositionInfo positionInfo; // Object for receiving information about open positions int countOrders; // Number of placed pending orders int countPositions; // Number of open positions CSymbolInfo symbolInfo; // Object for obtaining data on the symbol properties int iVolumesHandle; // Tick volume indicator handle double volumes[]; // Receiver array of indicator values (volumes themselves) };
Die Funktionen OnInit() und OnTick() werden zu den öffentlichen Methoden Init() und Tick(), und alle anderen Funktionen werden zu neuen privaten Methoden der Klasse CSimpleVolumesStrategy. Öffentliche Methoden können für Strategien von externem Code aufgerufen werden, zum Beispiel von EA-Objektmethoden. Private Methoden können nur von Methoden einer bestimmten Klasse aufgerufen werden. Fügen wir der Klassenbeschreibung die Methodenköpfe hinzu.
class CSimpleVolumeStrategy : public CStrategy { private: //--- ... previous code double volumes[]; // Receiver array of indicator values (volumes themselves) //--- Methods void UpdateCounts(); // Calculate the number of open orders and positions int SignalForOpen(); // Signal for opening pending orders void OpenBuyOrder(); // Open the BUY_STOP order void OpenSellOrder(); // Open the SELL_STOP order double ArrayAverage( const double &array[]); // Average value of the number array public: //--- Public methods virtual int Init(); // Strategy initialization method virtual void Tick(); // OnTick event handler };
An den Stellen, an denen sich die Implementierung dieser Funktionen befindet, fügen wir den Präfix „CSimpleVolumesStrategy::“ zu ihrem Namen hinzu, um dem Compiler zu verdeutlichen, dass es sich nicht mehr nur um Funktionen, sondern um Methoden unserer Klasse handelt.
class CSimpleVolumeStrategy : public CStrategy { // Class description listing properties and methods... }; int CSimpleVolumeStrategy::Init() { // Function code ... } void CSimpleVolumeStrategy::Tick() { // Function code ... } void CSimpleVolumeStrategy::UpdateCounts() { // Function code ... } int CSimpleVolumeStrategy::SignalForOpen() { // Function code ... } void CSimpleVolumeStrategy::OpenBuyOrder() { // Function code ... } void CSimpleVolumeStrategy::OpenSellOrder() { // Function code ... } double CSimpleVolumeStrategy::ArrayAverage(const double &array[]) { // Function code ... }
In dem ursprünglichen, einfachen EA wurden die Eingabewerte bei der Deklaration zugewiesen. Beim Start des kompilierten EAs wurden ihnen die Werte aus dem Eingabeparameter-Dialog zugewiesen (nicht die im Code eingestellten). Da dies in der Klassenbeschreibung nicht möglich ist, kommt hier der Konstruktor ins Spiel.
Erstellen wir einen Konstruktor mit der erforderlichen Liste von Parametern. Der Konstruktor sollte ebenfalls öffentlich sein, da wir sonst nicht in der Lage sind, Strategieobjekte aus einem externen Code zu erstellen.
class CSimpleVolumeStrategy : public CStrategy { private: //--- ... previous code public: //--- Public methods CSimpleVolumeStrategy( ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot, int p_signalPeriod, double p_signalDeviation, double p_signaAddlDeviation, int p_openDistance, double p_stopLevel, double p_takeLevel, int p_ordersExpiration, int p_maxCountOfOrders ); // Constructor virtual int Init(); // Strategy initialization method virtual void Tick(); // OnTick event handler };
Die Klassenbeschreibung ist fertig. Für alle Methoden gibt es bereits eine Implementierung, außer für den Konstruktor. Fügen wir das hinzu. Im einfachsten Fall weist der Konstruktor dieser Klasse nur die Werte der erhaltenen Parameter den entsprechenden Mitgliedern der Klasse zu. Außerdem werden die ersten vier Parameter dies durch den Aufruf des Konstruktors der Basisklasse tun.
CSimpleVolumeStrategy::CSimpleVolumeStrategy( ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot, int p_signalPeriod, double p_signalDeviation, double p_signaAddlDeviation, int p_openDistance, double p_stopLevel, double p_takeLevel, int p_ordersExpiration, int p_maxCountOfOrders) : // Initialization list CStrategy(p_magic, p_symbol, p_timeframe, p_fixedLot), // Call the base class constructor signalPeriod_(p_signalPeriod), signalDeviation_(p_signalDeviation), signaAddlDeviation_(p_signaAddlDeviation), openDistance_(p_openDistance), stopLevel_(p_stopLevel), takeLevel_(p_takeLevel), ordersExpiration_(p_ordersExpiration), maxCountOfOrders_(p_maxCountOfOrders) {}
Es gibt nur noch sehr wenig zu tun. Wir nehmen Umbenennungen von fixedLot_ und magicN_ in m_fixedLot und m_magic an allen Stellen, an denen sie vorkommen vor. Wir ersetzen die Verwendung der Funktion zum Abrufen des aktuellen Symbols Symbol() durch die Basisklassenvariable m_symbol und die Konstante PERIOD_CURRENT durch m_timeframe. Diesen Code speichern wir in der Datei SimpleVolumesStrategy.mqh im aktuellen Ordner.
Die Klasse für den EA
Erstellen wir die Basisklasse CAdvisor. Sein Ziel ist es, die Liste der Objekte bestimmter Handelsstrategien zu speichern und ihre Ereignisbehandler zu starten. Für diese Klasse wäre der Name CExpert angemessener, aber er wird bereits in der Standardbibliothek verwendet, sodass wir stattdessen CAdvisor verwenden werden.
#include "Strategy.mqh" class CAdvisor : public CObject { protected: CStrategy *m_strategies[]; // Array of trading strategies int m_strategiesCount;// Number of strategies public: virtual int Init(); // EA initialization method virtual void Tick(); // OnTick event handler virtual void Deinit(); // Deinitialization method void AddStrategy(CStrategy &strategy); // Strategy adding method };
In den Methoden Init() und Tick() werden alle Strategien aus dem Array m_strategies[] in einer Schleife durchlaufen und die entsprechenden Ereignisbehandlungsmethoden für sie aufgerufen.
void CAdvisor::Tick(void) { // Call OnTick handling for all strategies for(int i = 0; i < m_strategiesCount; i++) { m_strategies[i].Tick(); } }
Bei der Methode des Hinzufügens von Strategien geschieht genau dies.
void CAdvisor::AddStrategy(CStrategy &strategy) { // Increase the strategy number counter by 1 m_strategiesCount = ArraySize(m_strategies) + 1; // Increase the size of the strategies array ArrayResize(m_strategies, m_strategiesCount); // Write a pointer to the strategy object to the last element m_strategies[m_strategiesCount - 1] = GetPointer(strategy); }
Speichern wir diesen Code in der Datei Advisor.mqh im aktuellen Ordner. Auf der Grundlage dieser Klasse ist es möglich, abgeleitete Klassen zu erstellen, die bestimmte Methoden zur Verwaltung mehrerer Strategien implementieren. Wir beschränken uns aber vorerst nur auf diese Basisklasse und mischen uns in keiner Weise in die Arbeit der einzelnen Strategien ein.
Der Handels-EA mit mehreren Strategien
Um einen Handels-EA zu schreiben, müssen wir nur ein globales EA-Objekt (der Klasse CAdvisor) erstellen.
Bei der Initialisierung in OnInit() erstellen wir Strategieobjekte mit den ausgewählten Parametern und fügen sie dem EA-Objekt hinzu. Danach rufen wir die Methode Init() des EA-Objekts auf, damit alle Strategien darin initialisiert werden.
In OnTick() und OnDeinit() werden dann die entsprechenden Methoden des EA-Objekts aufgerufen.
#include "Advisor.mqh" #include "SimpleVolumesStartegy.mqh" input double depoPart_ = 0.8; // Part of the deposit for one strategy input ulong magic_ = 27182; // Magic CAdvisor expert; // EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { expert.AddStrategy(...); expert.AddStrategy(...); int res = expert.Init(); // Initialization of all EA strategies return(res); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { expert.Tick(); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { expert.Deinit(); } //+------------------------------------------------------------------+
Betrachten wir nun die Erstellung von Strategieobjekten im Detail. Da jede Instanz der Strategie ihre eigenen Aufträge und Positionen eröffnet und kontrolliert, sollten sie unterschiedliche Magic-Nummer haben. Die Magic-Nummer ist der erste Parameter des Strategiekonstruktors. Um eine unterschiedliche Magic-Nummer zu garantieren, werden wir daher verschiedene Zahlen zu der im Parameter magic_ angegebenen ursprünglichen Magic-Nummer hinzufügen.
expert.AddStrategy(new CSimpleVolumeStrategy(magic_ + 1, ...)); expert.AddStrategy(new CSimpleVolumeStrategy(magic_ + 2, ...));
Der zweite und dritte Konstruktorsparameter sind das Symbol und die Periode. Da wir die Optimierung für EURGBP H1 durchgeführt haben, geben wir diese spezifischen Werte an.
expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 1, "EURGBP", PERIOD_H1, ...)); expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 2, "EURGBP", PERIOD_H1, ...));
Der nächste wichtige Parameter ist die Größe der zu eröffnenden Positionen. Wir haben bereits die angemessene Größe für zwei Strategien (0,34 und 0,10) berechnet. Dies ist jedoch die Größe, mit der ein Drawdown von bis zu 10 % von 10.000 USD bei getrennt arbeitenden Strategien bewältigt werden kann. Wenn zwei Strategien gleichzeitig arbeiten, kann der Drawdown der ersten zum Drawdown der zweiten addiert werden. Im schlimmsten Fall müssen wir die Größe der eröffneten Positionen halbieren, um innerhalb der angegebenen 10 % zu bleiben. Es kann jedoch vorkommen, dass die Drawdowns der beiden Strategien nicht übereinstimmen oder sich sogar etwas ausgleichen. In diesem Fall müssen wir die Positionsgröße nicht so stark reduzieren und trotzdem werden die 10% nicht überschreiten. Deshalb machen wir den Reduktionsmultiplikator zu einem EA-Parameter (depoPart_), für den wir dann den optimalen Wert auswählen.
Die verbleibenden Parameter des Strategiekonstruktors sind die Werte, die wir nach der Optimierung des einfachen EA ausgewählt haben. Die endgültigen Ergebnisse lauten wie folgt:
expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 1, "EURGBP", PERIOD_H1, NormalizeDouble(0.34 * depoPart_, 2), 130, 0.9, 1.4, 231, 3750, 50, 600, 3) ); expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 2, "EURGBP", PERIOD_H1, NormalizeDouble(0.10 * depoPart_, 2), 159, 1.7, 0.8, 248, 3600, 495, 39000, 3) );
Wir speichern den resultierenden Code in der Datei SimpleVolumesExpert.mq5 im aktuellen Ordner.
Testergebnisse
Bevor wir den kombinierten EA testen, sollten wir uns daran erinnern, dass die Strategie mit dem ersten Parametersatz einen Gewinn von ca. 91 % und mit dem zweiten Parametersatz von 72 % hätte erzielen müssen (bei einer Starteinlage von 10.000 USD und einem maximalen Drawdown von 10 % (1.000 USD) und einem optimalen Losgröße).
Wählen wir den optimalen Wert des Parameters depoPart_ nach dem Kriterium der Aufrechterhaltung eines bestimmten Drawdowns aus, so erhalten wir die folgenden Ergebnisse.
Abb. 3. Ergebnis der kombinierten EA-Operation
Der Saldo am Ende des Testzeitraums betrug rund 22.400 USD, was einen Gewinn von 124 % bedeutet. Das ist mehr, als wir bei der Ausführung einzelner Instanzen dieser Strategie erhielten. Wir waren in der Lage, die Handelsergebnisse zu verbessern, indem wir nur mit der bestehenden Handelsstrategie arbeiteten, ohne sie zu verändern.
Schlussfolgerung
Wir haben nur einen kleinen Schritt zur Erreichung unseres Ziels getan. Sie hat unsere Zuversicht gestärkt, dass dieser Ansatz die Qualität des Handels verbessern kann. Bislang fehlen dem EA viele wichtige Aspekte.
Wir haben zum Beispiel eine sehr einfache Strategie besprochen, die das Schließen von Positionen in keiner Weise steuert, die ohne die Notwendigkeit, den Beginn des Balkens genau zu bestimmen, arbeitet und die keine komplizierten Berechnungen verwendet. Um den Zustand nach dem Neustart des Terminals wiederherzustellen, müssen wir keine weiteren Anstrengungen unternehmen, außer dem Zählen der offenen Positionen und Aufträge, das der EA übernehmen kann. Aber nicht jede Strategie wird so einfach sein. Darüber hinaus kann der EA nicht auf Netting-Konten arbeiten und kann entgegengesetzte Positionen gleichzeitig offen halten. Die Arbeit an verschiedenen Symbolen haben wir nicht in Betracht gezogen. Und so weiter und so fort...
Diese Aspekte sollten unbedingt berücksichtigt werden, bevor der eigentliche Handel beginnt. Bleiben Sie also dran für die neuen Artikel.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/14026
- 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.