English Русский Español 日本語 Português
preview
Entwicklung eines Expertenberaters für mehrere Währungen (Teil 1): Zusammenarbeit von mehreren Handelsstrategien

Entwicklung eines Expertenberaters für mehrere Währungen (Teil 1): Zusammenarbeit von mehreren Handelsstrategien

MetaTrader 5Handel | 12 Juni 2024, 13:41
109 0
Yuriy Bykov
Yuriy Bykov

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

    Beigefügte Dateien |
    SimpleVolumes.mq5 (21.22 KB)
    Strategy.mqh (3.78 KB)
    Advisor.mqh (6.29 KB)
    Neuronale Netze leicht gemacht (Teil 71): Zielkonditionierte prädiktive Kodierung (Goal-Conditioned Predictive Coding, GCPC) Neuronale Netze leicht gemacht (Teil 71): Zielkonditionierte prädiktive Kodierung (Goal-Conditioned Predictive Coding, GCPC)
    In früheren Artikeln haben wir die Decision-Transformer-Methode und mehrere davon abgeleitete Algorithmen besprochen. Wir haben mit verschiedenen Zielsetzungsmethoden experimentiert. Während der Experimente haben wir mit verschiedenen Arten der Zielsetzung gearbeitet. Die Studie des Modells über die frühere Trajektorie blieb jedoch immer außerhalb unserer Aufmerksamkeit. In diesem Artikel. Ich möchte Ihnen eine Methode vorstellen, die diese Lücke füllt.
    Kausalschluss in den Problemen bei Zeitreihenklassifizierungen Kausalschluss in den Problemen bei Zeitreihenklassifizierungen
    In diesem Artikel werden wir uns mit der Theorie des Kausalschlusses unter Verwendung von maschinellem Lernen sowie mit der Implementierung des nutzerdefinierten Ansatzes in Python befassen. Kausalschlüsse und kausales Denken haben ihre Wurzeln in der Philosophie und Psychologie und spielen eine wichtige Rolle für unser Verständnis der Realität.
    Algorithmen zur Optimierung mit Populationen: Binärer genetischer Algorithmus (BGA). Teil I Algorithmen zur Optimierung mit Populationen: Binärer genetischer Algorithmus (BGA). Teil I
    In diesem Artikel werden wir verschiedene Methoden untersuchen, die in binären genetischen und anderen Populationsalgorithmen verwendet werden. Wir werden uns die Hauptkomponenten des Algorithmus, wie Selektion, Crossover und Mutation, und ihre Auswirkungen auf die Optimierung ansehen. Darüber hinaus werden wir Methoden der Datendarstellung und ihre Auswirkungen auf die Optimierungsergebnisse untersuchen.
    Neuronale Netze leicht gemacht (Teil 70): Operatoren der Closed-Form Policy Improvement (CFPI) Neuronale Netze leicht gemacht (Teil 70): Operatoren der Closed-Form Policy Improvement (CFPI)
    In diesem Artikel werden wir uns mit einem Algorithmus vertraut machen, der geschlossene Operatoren zur Verbesserung der Politik verwendet, um die Aktionen des Agenten im Offline-Modus zu optimieren.