English Русский 日本語
preview
Entwicklung eines Expertenberaters für mehrere Währungen (Teil 3): Überarbeitung der Architektur

Entwicklung eines Expertenberaters für mehrere Währungen (Teil 3): Überarbeitung der Architektur

MetaTrader 5Handel | 19 Juni 2024, 14:36
85 0
Yuriy Bykov
Yuriy Bykov

Einführung

In den vorangegangenen Artikeln haben wir begonnen, einen Multiwährungs-EA zu entwickeln, der gleichzeitig mit verschiedenen Handelsstrategien arbeitet. Die im zweiten Artikel vorgestellte Lösung unterscheidet sich bereits erheblich von der im ersten Artikel vorgestellten. Dies zeigt, dass wir noch auf der Suche nach den besten Optionen sind.

Versuchen wir, das entwickelte System als Ganzes zu betrachten und von den kleinen Details der Implementierung zu abstrahieren, um zu verstehen, wie man es verbessern kann. Verfolgen wir dazu die wenn auch kurze, so doch spürbare Entwicklung des Systems.


Erste Betriebsart

Wir haben ein EA-Objekt (der Klasse CAdvisor oder ihrer Nachkommen) zugewiesen, das ein Aggregator von Handelsstrategieobjekten (Klasse CStrategy oder ihrer Abkömmlinge) ist. Zu Beginn der EA-Operation geschieht Folgendes in OnInit():

  • EA-Objekt wird erstellt.
  • Objekte von Handelsstrategien werden erstellt und dem EA in seinem Array für Handelsstrategien hinzugefügt.

In OnTick() geschieht Folgendes:

  • Die Methode CAdvisor::Tick() wird für das EA-Objekt aufgerufen.
  • Diese Methode durchläuft alle Strategien und ruft deren Methode CStrategy::Tick() auf.
  • Die Strategien innerhalb von CStrategy::Tick() führen alle notwendigen Operationen zum Öffnen und Schließen von Marktpositionen aus.

Dies kann schematisch wie folgt dargestellt werden:



Abb. 1. Funktionsweise des ersten Artikels

Abb. 1. Betriebsart des ersten Artikels

Der Vorteil dieses Modus war, dass es möglich war, den EA über eine Reihe relativ einfacher Operationen mit anderen Instanzen von Handelsstrategien arbeiten zu lassen, vorausgesetzt, man hatte den Quellcode eines EA, der einer bestimmten Handelsstrategie folgte.

Der größte Nachteil hat sich jedoch schnell herausgestellt: Bei der Kombination mehrerer Strategien müssen wir die Größe der Positionen, die von jeder Instanz der Strategie eröffnet werden, mehr oder weniger reduzieren. Dies kann dazu führen, dass einige oder sogar alle Strategiefälle vollständig vom Handel ausgeschlossen werden. Je mehr Strategieinstanzen wir in die parallele Arbeit einbeziehen oder je kleiner die anfängliche Einlage ist, desto wahrscheinlicher ist ein solches Ergebnis, da die Mindestgröße der offenen Marktpositionen festgelegt ist.

Außerdem kam es bei der Zusammenarbeit mehrerer Strategieinstanzen zu einer Situation, in der entgegengesetzte Positionen der gleichen Größe geöffnet wurden. Bezogen auf das Gesamtvolumen entspricht dies dem Fehlen offener Positionen, aber es wurden weiterhin Swaps auf gegenläufige offene Positionen getätigt.


Zweite Betriebsart

Um die Unzulänglichkeiten zu beseitigen, haben wir beschlossen, alle Operationen mit Marktpositionen an einen separaten Ort zu verlagern, sodass die Handelsstrategien keine Möglichkeit mehr haben, direkt Marktpositionen zu eröffnen. Dies erschwert zwar die Überarbeitung fertiger Strategien, ist aber kein großer Verlust, da der Hauptnachteil des ersten Modus dadurch ausgeglichen wird.

Zwei neue Entitäten tauchen in unserem Betriebsmodus auf: virtuelle Positionen ( Klasse CVirtualOrder ) und ein Empfänger von Handelsvolumina aus Strategien (Klasse CReceiver und ihre Nachkommen).

Zu Beginn der EA-Operation geschieht Folgendes in OnInit():

  • Ein Empfängerobjekt wird erstellt.
  • Ein EA-Objekt wird erstellt und der erstellte Empfänger wird an dieses übergeben.
  • Objekte von Handelsstrategien werden erstellt und dem EA in seinem Array für Handelsstrategien hinzugefügt.
  • Jede Strategie erstellt ein eigenes Array von virtuellen Positionsobjekten mit der erforderlichen Anzahl dieser Objekte.

In OnTick() geschieht Folgendes:

  • Die Methode CAdvisor::Tick() wird für das EA-Objekt aufgerufen.
  • Diese Methode durchläuft alle Strategien und ruft deren Methode CStrategy::Tick() auf.
  • Die Strategien innerhalb von CStrategy::Tick() führen alle notwendigen Operationen zum Öffnen und Schließen virtueller Positionen aus. Wenn ein Ereignis eintritt, das mit einer Änderung in der Zusammensetzung der offenen virtuellen Positionen verbunden ist, merkt sich die Strategie diese Änderung, indem sie ein Flag setzt.
  • Wenn mindestens eine Strategie ein Änderungs-Flag gesetzt hat, startet der Empfänger die Methode zur Anpassung der offenen Volumina von Marktpositionen. Wenn die Anpassung erfolgreich ist, wird das Änderungs-Flag für alle Strategien zurückgesetzt.

Dies kann schematisch wie folgt dargestellt werden:

Abb. 2. Funktionsweise vom zweiten Artikel

Abb. 2. Betriebsart vom zweiten Artikel

In dieser Betriebsart werden wir nicht mehr mit der Tatsache konfrontiert, dass eine bestimmte Strategie in keiner Weise die Größe der offenen Marktpositionen beeinflusst. Im Gegenteil, selbst eine Instanz, die ein sehr kleines virtuelles Volumen eröffnet, kann zu jenem Tropfen werden, der das Gesamtvolumen der virtuellen Positionen von mehreren Strategieinstanzen über das minimal zulässige Volumen einer Marktposition hinaus überschreitet. In diesem Fall wird die tatsächliche Marktposition eröffnet.

Auf dem Weg dorthin erhielten wir weitere angenehme Änderungen, darunter mögliche Einsparungen bei Swaps, eine geringere Belastung des Depots, weniger beobachtete Drawdowns und eine verbesserte Bewertung der Handelsqualität (Sharpe Ratio, Profitfaktor).

Beim Testen des zweiten Modus ist mir Folgendes aufgefallen:

  • Jede Strategie behandelt zunächst bereits offene virtuelle Positionen, um die ausgelösten StopLoss- und TakeProfit-Levels zu bestimmen. Wenn eines der Niveaus erreicht ist, wird eine solche virtuelle Position geschlossen. Daher wurde diese Handhabung sofort in die Methode der Klasse CVirtualOrder verlagert. Aber diese Lösung scheint immer noch eine unzureichende Verallgemeinerung zu sein.
  • Wir haben die Zusammensetzung der Basisklassen erweitert, indem wir neue erforderliche Entitäten hinzugefügt haben. Wenn wir keine virtuellen Positionen behandeln wollen, können wir solche Basisklassen trotzdem verwenden, indem wir ihnen einfach „leere“ Objekte übergeben. Wir können zum Beispiel das Objekt der Klasse CReceiver erstellen, das nur einfache, leere Methodenstrukturen enthält. Aber auch dies scheint eher eine Übergangslösung zu sein, die überarbeitet werden muss.
  • Wir haben der Basisklasse CStrategy neue Methoden und die Eigenschaft für die Verfolgung von Änderungen in der Zusammensetzung offener virtueller Positionen hinzugefügt, was sich auf die Verwendung dieser Methoden in der Basisklasse CAdvisor ausgewirkt hat. Auch hier sieht es so aus, als würden die Möglichkeiten eingeschränkt und eine zu spezifische Implementierung in der Basisklasse vorgeschrieben.
  • Die Basisklasse CStrategy erhielt die Methode Volume(), die das Gesamtvolumen der offenen virtuellen Positionen zurückgibt, da die entwickelte Empfängerklasse CVolumeReceiver Daten über die offenen virtuellen Volumina der einzelnen Strategien benötigte. Damit haben wir jedoch die Möglichkeit abgeschnitten, virtuelle Positionen für mehrere Symbole innerhalb einer Instanz einer Handelsstrategie zu eröffnen. In diesem Fall verliert das Gesamtvolumen seine Bedeutung. Diese Lösung eignet sich zum Testen von Ein-Symbol-Strategien, mehr aber auch nicht.
  • Wir haben das Array für die Speicherung der Zeiger auf EA-Strategien in der CReceiver-Klasse verwendet, damit der Empfänger sie verwenden kann, um das offene virtuelle Volumen der Strategien herauszufinden. Dies führte zu einer Verdoppelung des Codes, der die Strategie-Arrays im EA und im Empfänger füllt.
  • Jede Strategie eröffnet Positionen eines einzelnen Symbols: Wenn Sie einen Empfänger zur Gruppe der Strategien hinzufügen, wird die Strategie nach ihrem Symbol gefragt, und es wird zur Gruppe der verwendeten Symbole hinzugefügt. Wir haben diese Funktion in der Klasse CVolumeReceiver verwendet. Der Empfänger arbeitet dann nur mit den Symbolen, die er in sein Symbolfeld aufgenommen hat. Wir haben bereits die Einschränkung erwähnt, die sich aus diesem Verhalten ergibt.
Auf der Grundlage der Analyse der aufgeführten Mängel und der Diskussion in den Kommentaren werden wir die folgenden Änderungen vornehmen:
  • Wir sollten die Basisklassen CStrategy und CAdvisor so weit wie möglich bereinigen. Schreiben wir die nutzerdefinierten, abgeleiteten Klassen CVirtualStrategy und CVirtualAdvisor, um den Entwicklungszweig der EAs mit virtuellem Handel zu erstellen. Sie werden nun unsere übergeordneten Klassen für spezifische Strategien und EAs sein.
  • Es ist an der Zeit, die Klasse der virtuellen Stellen zu erweitern. Jede virtuelle Position sollte einen Zeiger auf ein Empfängerobjekt erhalten, das dafür verantwortlich ist, das virtuelle Handelsvolumen auf den Markt zu bringen, und ein Handelsstrategieobjekt, das Entscheidungen über das Öffnen/Schließen einer virtuellen Position trifft. Auf diese Weise können interessierte Stellen über die Eröffnung und Schließung virtueller Stellen informiert werden.
  • Verschieben wir die Speicherung aller virtuellen Positionen in ein Array, anstatt sie auf mehrere Arrays zu verteilen, die zu Strategieinstanzen gehören. Jede Strategieinstanz wird für ihre Tätigkeit mehrere Elemente aus diesem Array anfordern. Der Eigentümer des gemeinsamen Arrays wird der Empfänger des Handelsvolumens sein.
  • In einem EA gibt es nur einen Empfänger. Daher werden wir es als Singleton implementieren. Seine einzige Instanz wird an allen notwendigen Stellen verfügbar sein. Wir werden diese Implementierung als die abgeleitete Klasse CVirtualReceiver formalisieren.
  • Wir fügen eine Reihe neuer Entitäten - Symbolempfänger (CVirtualSymbolReceiver-Klasse) - in den Empfänger ein. Jeder Symbol-Empfänger arbeitet nur mit den virtuellen Positionen seines Symbols, das automatisch an den Symbol-Empfänger angehängt wird, wenn er geöffnet wird, und wieder gelöst wird, wenn er geschlossen wird.
Versuchen wir, dies alles umzusetzen.


Bereinigung von Basisklassen

Lassen wir nur das Wesentliche in den Basisklassen CStrategy und CAdvisor. Im Falle von CStartegy lassen wir nur die Methode für die Behandlung des OnTick-Ereignisses stehen und erhalten den folgenden prägnanten Code:

//+------------------------------------------------------------------+
//| Base class of the trading strategy                               |
//+------------------------------------------------------------------+
class CStrategy {
public:
   virtual void      Tick() = 0; // Handle OnTick events
};

Alles andere wird in den abgeleiteten der Klasse untergebracht.

Binden wir in die Basisklasse CAdvisor eine kleine Datei Macros.mqh ein, die mehrere nützliche Makros zur Durchführung von Operationen mit regulären Arrays enthält:

  • APPEND(A, V) — fügt das Element V an das Ende des Arrays A an;
  • FIND(A, V, I) — schreibt das Array-Element A, das gleich A ist, in die Variable I. Wird das Element nicht gefunden, so wird -1 in der Variablen I gespeichert;
  • ADD(A, V) — fügt das Element V am Ende des Arrays A hinzu, wenn sich ein solches Element nicht bereits im Array befindet;
  • FOREACH(A, D) — Schleife durch die Array-Indizes von A (der Index befindet sich in der lokalen Variablen i), wobei D-Aktionen im Körper ausgeführt werden;
  • REMOVE_AT(A, I) — entfernt ein Element aus dem Array A an einer I-Indexposition, wobei nachfolgende Elemente verschoben und die Arraygröße verringert wird;
  • REMOVE(A, V) — entfernt ein Element mit dem Wert V aus dem Array A

// Useful macros for array operations
#ifndef __MACROS_INCLUDE__
#define APPEND(A, V)    A[ArrayResize(A, ArraySize(A) + 1) - 1] = V;
#define FIND(A, V, I)   { for(I=ArraySize(A)-1;I>=0;I--) { if(A[I]==V) break; } }
#define ADD(A, V)       { int i; FIND(A, V, i) if(i==-1) { APPEND(A, V) } }
#define FOREACH(A, D)   { for(int i=0, im=ArraySize(A);i<im;i++) {D;} }
#define REMOVE_AT(A, I) { int s=ArraySize(A);for(int i=I;i<s-1;i++) { A[i]=A[i+1]; } ArrayResize(A, s-1);}
#define REMOVE(A, V)    { int i; FIND(A, V, i) if(i>=0) REMOVE_AT(A, i) }
#define __MACROS_INCLUDE__
#endif
//+------------------------------------------------------------------+

Diese Makros werden in anderen Dateien verwendet, um den Code kompakter und lesbarer zu machen und den Aufruf zusätzlicher Funktionen zu vermeiden.

Wir werden alle Stellen, an denen der Empfänger angetroffen wurde, aus der CAdvisor-Klasse entfernen und nur den Aufruf der entsprechenden Strategie-Handler in der OnTick-Ereignisbehandlungsmethode belassen. Wir werden den folgenden Code erhalten:

#include "Macros.mqh"
#include "Strategy.mqh"

//+------------------------------------------------------------------+
//| EA base class                                                    |
//+------------------------------------------------------------------+
class CAdvisor {
protected:
   CStrategy         *m_strategies[];  // Array of trading strategies
public:
                    ~CAdvisor();                // Destructor
   virtual void      Tick();                    // OnTick event handler
   virtual void      Add(CStrategy *strategy);  // Method for adding a strategy
};

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CAdvisor::~CAdvisor() {
// Delete all strategy objects
   FOREACH(m_strategies, delete m_strategies[i]);
}

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CAdvisor::Tick(void) {
// Call OnTick handling for all strategies
   FOREACH(m_strategies, m_strategies[i].Tick());
}

//+------------------------------------------------------------------+
//| Strategy adding method                                           |
//+------------------------------------------------------------------+
void CAdvisor::Add(CStrategy *strategy) {
   APPEND(m_strategies, strategy);  // Add the strategy to the end of the array
}
//+------------------------------------------------------------------+

Diese Klassen verbleiben in den Dateien Strategy.mqh und Advisor.mqh im aktuellen Ordner.

Verschieben wir nun den notwendigen Code in die abgeleiteten Strategie- und EA-Klassen, die mit virtuellen Positionen arbeiten sollen.

Erstellen Sie die von CStrategy geerbte Klasse CVirtualStrategy. Fügen wir ihr die folgenden Felder und Methoden hinzu:

  • ein Array für die virtuellen Positionen (Aufträgen);
  • Gesamtzahl der offenen Positionen und Aufträge;
  • Methode zur Zählung offener virtueller Positionen und Aufträge;
  • Methoden zur Handhabung des Öffnens/Schließens einer virtuellen Position (Auftrag).
Im Moment rufen die Methoden für die Bearbeitung von offenen/geschlossenen virtuellen Positionen einfach die Methode für die Neuberechnung offener virtueller Positionen auf, die den Wert des Feldes m_ordersTotal aktualisiert. Es müssen noch keine weiteren Aktionen durchgeführt werden. Wahrscheinlich werden wir das später tun müssen. Daher werden diese Methoden vorerst von der Methode der Zählung offener virtueller Positionen getrennt.

#include "Strategy.mqh"
#include "VirtualOrder.mqh"

//+------------------------------------------------------------------+
//| Class of a trading strategy with virtual positions               |
//+------------------------------------------------------------------+
class CVirtualStrategy : public CStrategy {
protected:
   CVirtualOrder     *m_orders[];   // Array of virtual positions (orders)
   int               m_ordersTotal; // Total number of open positions and orders

   virtual void      CountOrders(); // Calculate the number of open positions and orders

public:
   virtual void      OnOpen();      // Event handler for opening a virtual position (order)
   virtual void      OnClose();     // Event handler for closing a virtual position (order)
};

//+------------------------------------------------------------------+
//| Counting open virtual positions and orders                       |
//+------------------------------------------------------------------+
void CVirtualStrategy::CountOrders() {
   m_ordersTotal = 0;
   FOREACH(m_orders, if(m_orders[i].IsOpen()) { m_ordersTotal += 1; })
}

//+------------------------------------------------------------------+
//| Event handler for opening a virtual position (order)             |
//+------------------------------------------------------------------+
void CVirtualStrategy::OnOpen() {
   CountOrders();
}

//+------------------------------------------------------------------+
//| Event handler for closing a virtual position (order)             |
//+------------------------------------------------------------------+
void CVirtualStrategy::OnClose() {
   CountOrders();
}

Speichern Sie diesen Code in der Datei VirtualStrategy.mqh im aktuellen Ordner.

Da wir die Arbeit mit dem Empfänger aus der Basisklasse CAdvisor entfernt haben, sollte sie auf unsere neue Unterklasse CVirtualAdvisor übertragen werden. In dieser Klasse fügen wir das Feld m_receiver hinzu, um den Zeiger auf das Objekt des Empfängers von Handelsvolumen zu speichern.

Im Konstruktor wird das Feld mit dem Zeiger auf das einzig mögliche Empfängerobjekt initialisiert, das genau dann erstellt wird, wenn die statische Methode CVirtualReceiver::Instance() aufgerufen wird. Der Destruktor sorgt dafür, dass das Objekt ordnungsgemäß gelöscht wird.

Wir werden auch neue Aktionen in OnTick hinzufügen. Bevor wir die Ereignisbehandlung für dieses Ereignis in den Strategien starten, werden wir zuerst die Ereignisbehandlung für dieses Ereignis im Empfänger starten. Nachdem das Ereignis von den Strategien verarbeitet wurde, starten wir die Methode des Empfängers, die die offenen Volumina anpasst. Wenn der Empfänger nun der Eigentümer aller virtuellen Positionen ist, kann er selbst das Vorhandensein von Änderungen feststellen. Daher gibt es keine Implementierung der Nachverfolgung von Änderungen in der Handelsstrategieklasse, weshalb wir sie nicht nur aus der Basisstrategieklasse, sondern vollständig entfernen.

#include "Advisor.mqh"
#include "VirtualReceiver.mqh"

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   CVirtualReceiver  *m_receiver; // Receiver object that brings positions to the market

public:
                     CVirtualAdvisor(ulong p_magic = 1); // Constructor
                    ~CVirtualAdvisor();                  // Destructor
   virtual void      Tick() override;                    // OnTick event handler

};

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1) :
// Initialize the receiver with a static receiver
   m_receiver(CVirtualReceiver::Instance(p_magic)) {};

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   delete m_receiver;         // Remove the recipient
}

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Tick(void) {
// Receiver handles virtual positions
   m_receiver.Tick();
   
// Start handling in strategies
   CAdvisor::Tick();
   
// Adjusting market volumes
   m_receiver.Correct();
}
//+------------------------------------------------------------------+

Wir speichern diesen Code in der Datei VirtualAdvisor.mqh im aktuellen Ordner.


Ausweitung der Klasse der virtuellen Positionen

Die virtuelle Positionsklasse erhält den Zeiger auf die Objekte m_receiver und m_strategy. Die Werte für diese Felder müssen über die Konstruktorparameter übergeben werden, daher werden wir auch hier Änderungen vornehmen. Ich habe auch ein paar „Getter“ für die privaten Eigenschaften der virtuellen Position hinzugefügt: Id() und Symbol(). Wir zeigen den hinzugefügten Code in der Klassenbeschreibung:

//+------------------------------------------------------------------+
//| Class of virtual orders and positions                            |
//+------------------------------------------------------------------+
class CVirtualOrder {
private:
//--- Static fields...
   
//--- Related recipient objects and strategies
   CVirtualReceiver  *m_receiver;
   CVirtualStrategy  *m_strategy;

//--- Order (position) properties ...
   
//--- Closed order (position) properties ...
   
//--- Private methods
   
public:
                     CVirtualOrder(
      CVirtualReceiver *p_receiver,
      CVirtualStrategy *p_strategy
   );                                  // Constructor

//--- Methods for checking the position (order) status ...
   

//--- Methods for receiving position (order) properties ...
   ulong             Id() {            // ID
      return m_id;
   }
   string            Symbol() {        // Symbol
      return m_symbol;
   }

//--- Methods for handling positions (orders) ...
  
};

In der Konstruktorimplementierung haben wir einfach zwei Zeilen in die Initialisierungsliste eingefügt, um die Werte neuer Felder aus den Konstruktorparametern festzulegen:

CVirtualOrder::CVirtualOrder(CVirtualReceiver *p_receiver, CVirtualStrategy *p_strategy) :
// Initialization list
   m_id(++s_count),  // New ID = object counter + 1
   m_receiver(p_receiver),
   m_strategy(p_strategy),
   ...,
   m_point(0) {
}

Die Benachrichtigung des Empfängers und der Strategie sollte nur erfolgen, wenn eine virtuelle Position eröffnet oder geschlossen wird. Dies geschieht nur in den Methoden Open() und Close(), also fügen wir ihnen ein wenig Code hinzu:

//+------------------------------------------------------------------+
//| Open a virtual position                                          |
//+------------------------------------------------------------------+
bool CVirtualOrder::Open(...) {
   // If the position is already open, then do nothing ...

   if(s_symbolInfo.Name(symbol)) {  // Select the desired symbol
      // Update information about current prices ...

      // Initialize position properties ...
  
      // Depending on the direction, set the opening price, as well as the SL and TP levels ...
            
      // Notify the recipient and the strategy that the position (order) is open
      m_receiver.OnOpen(GetPointer(this));
      m_strategy.OnOpen();

      ...

      return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| Close a position                                                 |
//+------------------------------------------------------------------+
void CVirtualOrder::Close() {
   if(IsOpen()) { // If the position is open
      ...
      // Define the closure reason to be displayed in the log ...
     
      // Save the close price depending on the type ...
    
      // Notify the recipient and the strategy that the position (order) is open
      m_receiver.OnClose(GetPointer(this));
      m_strategy.OnClose();
   }
}

Wir übergeben den Zeiger auf das aktuelle virtuelle Positionsobjekt an die OnOpen() und OnClose() Empfänger-Handler. Bei den Strategie-Handlern war dies bisher nicht erforderlich, sodass sie ohne Parameter implementiert sind.

Dieser Code verbleibt im aktuellen Ordner in der Datei mit demselben Namen — VirtualOrder.mqh.


Einführen eines neuen Empfängers

Beginnen wir mit der Implementierung der Empfängerklasse CVirtualReceiver, um die Einzigartigkeit einer Instanz einer bestimmten Klasse zu gewährleisten. Dazu werden wir ein Standardentwurfsmuster namens Singleton verwenden. Das werden wir tun müssen:

  • den Klassenkonstruktor nicht-öffentlich machen;
  • ein statisches Klassenfeld hinzufügen, das den Zeiger auf das Klassenobjekt speichert;
  • eine statische Methode hinzufügen, die, falls nicht vorhanden, eine Instanz dieser Klasse erzeugt oder eine bereits vorhandene zurückgibt.

//+------------------------------------------------------------------+
//| Class for converting open volumes to market positions (receiver) |
//+------------------------------------------------------------------+
class CVirtualReceiver : public CReceiver {
protected:
// Static pointer to a single class instance
   static   CVirtualReceiver *s_instance;

   ...

   CVirtualReceiver(ulong p_magic = 0);   // Private constructor

public:
//--- Static methods
   static
   CVirtualReceiver  *Instance(ulong p_magic = 0);    // Singleton - creating and getting a single instance

   ...
};

// Initializing a static pointer to a single class instance
CVirtualReceiver *CVirtualReceiver::s_instance = NULL;


//+------------------------------------------------------------------+
//| Singleton - creating and getting a single instance               |
//+------------------------------------------------------------------+
CVirtualReceiver* CVirtualReceiver::Instance(ulong p_magic = 0) {
   if(!s_instance) {
      s_instance = new CVirtualReceiver(p_magic);
   }
   return s_instance;
}

Als Nächstes fügen wir der Klasse das Array m_orders zur Speicherung aller virtuellen Positionen hinzu. Jede Strategieinstanz fordert vom Empfänger eine bestimmte Anzahl virtueller Positionen an. Wir fügen außerdem noch die statische Methode Get() hinzu, die die erforderliche Anzahl von virtuellen Positionsobjekten erzeugt und Zeiger auf diese Objekte in das Empfänger-Array und das virtuelle Positions-Array der Strategie einfügt:

class CVirtualReceiver : public CReceiver {
protected:
   ...
   CVirtualOrder     *m_orders[];         // Array of virtual positions
   
   ...

public:
//--- Static methods
   ...
   static void       Get(CVirtualStrategy *strategy,
                         CVirtualOrder *&orders[],
                         int n); // Allocate the necessary amount of virtual positions to the strategy
   ...
};

...

//+------------------------------------------------------------------+
//| Allocate the necessary amount of virtual positions to strategy   |
//+------------------------------------------------------------------+
static void CVirtualReceiver::Get(CVirtualStrategy *strategy,   // Strategy
                                  CVirtualOrder *&orders[],     // Array of strategy positions
                                  int n                         // Required number
                                 ) {
   CVirtualReceiver *self = Instance();   // Receiver singleton
   ArrayResize(orders, n);                // Expand the array of virtual positions
   FOREACH(orders,
           orders[i] = new CVirtualOrder(self, strategy); // Fill the array with new objects
           APPEND(self.m_orders, orders[i])) // Register the created virtual position
   ...
}

Nun ist es an der Zeit, das Array für Zeiger auf Symbolempfängerobjekte (die Klasse CVirtualSymbolReceiver ) in die Klasse aufzunehmen. Diese Klasse wurde noch nicht erstellt, aber wir wissen bereits, was sie tun soll - Marktpositionen direkt öffnen und schließen in Übereinstimmung mit virtuellen Volumen für ein einzelnes Symbol. Daher kann man sagen, dass die Anzahl der Symbolempfänger-Objekte gleich der Anzahl der verschiedenen im EA verwendeten Symbole ist. Wir leiten die Klasse von CReceiver ab, sodass sie über die Methode Correct() verfügt, die die wichtigste, nützliche Arbeit leistet. Wir werden auch die notwendigen Hilfsmethoden hinzufügen.

Lassen wir dies für später und kehren wir zur Klasse CVirtualReceiver zurück und fügen ihr die virtuelle Überschreibung der Methode Correct() hinzu.

class CVirtualReceiver : public CReceiver {
protected:
   ...
   CVirtualSymbolReceiver *m_symbolReceivers[];       // Array of recipients for individual symbols

public:
   ...
//--- Public methods
   virtual bool      Correct() override;              // Adjustment of open volumes
};

Die Implementierung der Methode Correct() ist nun recht einfach, da wir die Hauptarbeit auf eine niedrigere Ebene der Hierarchie verlagern. Im Moment reicht es aus, eine Schleife durch alle Symbolempfänger zu ziehen und deren Methode Correct() aufzurufen.

Um die Anzahl der unnötigen Aufrufe zu reduzieren, fügen wir eine vorläufige Prüfung hinzu, dass der Handel nun generell erlaubt ist, indem wir die Methode IsTradeAllowed() hinzufügen, die die Frage beantwortet. Wir werden auch das Klassenfeld m_isChanged hinzufügen, das als Flag für Änderungen in offenen virtuellen Positionen dienen soll. Wir werden sie auch überprüfen, bevor wir eine Anpassung verlangen.

class CVirtualReceiver : public CReceiver {
   ...
   bool              m_isChanged;         // Are there any changes in open positions?
   ...
   bool              IsTradeAllowed();    // Is trading available?

public:
   ...

   virtual bool      Correct() override;  // Adjustment of open volumes
};
//+------------------------------------------------------------------+
//| Adjust open volumes                                              |
//+------------------------------------------------------------------+
bool CVirtualReceiver::Correct() {
   bool res = true;
   if(m_isChanged && IsTradeAllowed()) {
      // If there are changes, then we call the adjustment of the recipients of individual symbols
      FOREACH(m_symbolReceivers, res &= m_symbolReceivers[i].Correct());
      m_isChanged = !res;
   }
   return res;
}

Wir überprüfen in der Methode IsTradeAllowed() den Status des Terminals und des Handelskontos, um festzustellen, ob ein echter Handel möglich ist:

//+------------------------------------------------------------------+
//| Is trading available?                                            |
//+------------------------------------------------------------------+
bool CVirtualReceiver::IsTradeAllowed() {
   return (true
           && MQLInfoInteger(MQL_TRADE_ALLOWED)
           && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)
           && AccountInfoInteger(ACCOUNT_TRADE_EXPERT)
           && AccountInfoInteger(ACCOUNT_TRADE_ALLOWED)
           && TerminalInfoInteger(TERMINAL_CONNECTED)
          );
}

Wir haben das Änderungs-Flag in der Methode Correct() gesetzt. Das Flag wird zurückgesetzt, wenn die Volumensanpassung erfolgreich war. Aber wo soll dieses Flag gesetzt werden? Dies sollte natürlich geschehen, wenn eine virtuelle Position geöffnet oder geschlossen wird. In der Klasse CVirtualOrder haben wir speziell die Aufrufe für die Methoden OnOpen() und OnClose(), die in der Klasse CVirtualReceiver noch nicht vorhanden sind, zu den open/close-Methoden hinzugefügt. Wir werden in ihnen das Flag für Änderungen setzen.

Darüber hinaus sollten wir den gewünschten Symbolempfänger über die Änderungen in diesen Handlern informieren. Beim Öffnen der allerersten, virtuellen Position für ein bestimmtes Symbol gibt es den entsprechenden Symbolempfänger noch nicht, sodass wir ihn erstellen und informieren müssen. Bei späteren Operationen zum Öffnen/Schließen virtueller Positionen für ein bestimmtes Symbol gibt es bereits einen entsprechenden Symbol-Empfänger, sodass wir diesen nur noch informieren müssen.

class CVirtualReceiver : public CReceiver {
   ...

public:
   ...

//--- Public methods
   void              OnOpen(CVirtualOrder *p_order);  // Handle virtual position opening
   void              OnClose(CVirtualOrder *p_order); // Handle virtual position closing
   ...
};

//+------------------------------------------------------------------+
//| Handle opening a virtual position                                |
//+------------------------------------------------------------------+
void CVirtualReceiver::OnOpen(CVirtualOrder *p_order) {
   string symbol = p_order.Symbol();      // Define position symbol
   CVirtualSymbolReceiver *symbolReceiver;
   int i;
   FIND(m_symbolReceivers, symbol, i);    // Search for the symbol recipient

   if(i == -1) {
      // If not found, then create a new recipient for the symbol
      symbolReceiver = new CVirtualSymbolReceiver(m_magic, symbol);
      // and add it to the array of symbol recipients 
      APPEND(m_symbolReceivers, symbolReceiver);
   } else {
      // If found, then take it
      symbolReceiver = m_symbolReceivers[i];
   }
   
   symbolReceiver.Open(p_order); // Notify the symbol recipient about the new position
   m_isChanged = true;           // Remember that there are changes
}

//+------------------------------------------------------------------+
//| Handle closing a virtual position                                |
//+------------------------------------------------------------------+
void CVirtualReceiver::OnClose(CVirtualOrder *p_order) {
   string symbol = p_order.Symbol();   // Define position symbol
   int i;
   FIND(m_symbolReceivers, symbol, i); // Search for the symbol recipient

   if(i != -1) {
      m_symbolReceivers[i].Close(p_order);   // Notify the symbol recipient about closing a position
      m_isChanged = true;                    // Remember that there are changes
   }
}

Zusätzlich zum Öffnen/Schließen von virtuellen Positionen auf der Grundlage von Handelsstrategiesignalen können diese geschlossen werden, wenn StopLoss- oder TakeProfit-Levels erreicht werden. In der Klasse CVirtualOrder gibt es speziell dafür die Methode Tick(). Er prüft die Level und schließt die virtuelle Position, falls erforderlich. Sie sollte bei jedem Tick und für alle virtuellen Positionen aufgerufen werden. Dies ist genau das, was die Methode Tick() in der Klasse CVirtualReceiver tun wird. Fügen wir die Klasse hinzu:

class CVirtualReceiver : public CReceiver {
   ...

public:
   ...

//--- Public methods
   void              Tick();     // Handle a tick for the array of virtual orders (positions)
   ...
};

//+------------------------------------------------------------------+
//| Handle a tick for the array of virtual orders (positions)        |
//+------------------------------------------------------------------+
void CVirtualReceiver::Tick() {
   FOREACH(m_orders, m_orders[i].Tick());
}

Achten wir am Ende darauf, den für virtuelle Positionsobjekte zugewiesenen Speicher korrekt wieder freizugeben. Da sie sich alle in dem Array m_orders befinden, fügen wir einen Destruktor hinzu, in dem wir sie löschen werden:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CVirtualReceiver::~CVirtualReceiver() {
   FOREACH(m_orders, delete m_orders[i]); // Remove virtual positions
}

Wir speichern den resultierenden Code in der Datei VirtualReceiver.mqh im aktuellen Ordner.


Implementierung eines Symbolempfängers

Es bleibt, die letzte Klasse CVirtualSymbolReceiver zu implementieren, damit die Betriebsart eine fertige, gebrauchstaugliche Form annimmt. Wir werden den Hauptinhalt von der Klasse CVolumeReceiver aus dem vorherigen Artikel übernehmen, wobei wir die Stellen entfernen, die sich auf die Bestimmung des Symbols jeder virtuellen Position und die Aufzählung der Symbole während der Anpassung beziehen.

Objekte dieser Klasse haben auch ihre eigenen Arrays mit Zeigern auf virtuelle Positionsobjekte, aber hier ändert sich ihre Zusammensetzung ständig. Wir verlangen, dass dieses Array nur offene virtuelle Positionen enthält. Dann wird klar, was beim Öffnen und Schließen einer virtuellen Position zu tun ist: Sobald die virtuelle Position geöffnet wird, sollten wir sie dem Array des entsprechenden Symbolempfängers hinzufügen, und sobald sie geschlossen wird, sollten wir sie aus dem Array entfernen.

Es wäre auch praktisch, wenn wir ein Flag für Änderungen in der Zusammensetzung der offenen virtuellen Positionen hätten. Auf diese Weise können unnötige Kontrollen bei jedem Tick vermieden werden.

Fügen wir der Klasse Felder für ein Symbol, ein Array von Positionen und eine Änderungsmarkierung sowie zwei Methoden für das Öffnen/Schließen hinzu:

class CVirtualSymbolReceiver : public CReceiver {
   string            m_symbol;         // Symbol
   CVirtualOrder     *m_orders[];      // Array of open virtual positions
   bool              m_isChanged;      // Are there any changes in the composition of virtual positions?

   ...   

public:
   ...
   void              Open(CVirtualOrder *p_order);    // Register opening a virtual position
   void              Close(CVirtualOrder *p_order);   // Register closing a virtual position 
   ...
};

Die Implementierung dieser Methoden selbst ist trivial: Wir fügen die übergebene virtuelle Position dem Array hinzu bzw. entfernen sie daraus und setzen das Flag für das Vorhandensein von Änderungen.

//+------------------------------------------------------------------+
//| Register opening a virtual position                              |
//+------------------------------------------------------------------+
void CVirtualSymbolReceiver::Open(CVirtualOrder *p_order) {
   APPEND(m_orders, p_order); // Add a position to the array
   m_isChanged = true;        // Set the changes flag
}

//+------------------------------------------------------------------+
//| Register closing a virtual position                              |
//+------------------------------------------------------------------+
void CVirtualSymbolReceiver::Close(CVirtualOrder *p_order) {
   REMOVE(m_orders, p_order); // Remove a position from the array
   m_isChanged = true;        // Set the changes flag
}

Außerdem müssen wir den gewünschten Symbolempfänger über einen Symbolnamen suchen. Um den normalen linearen Suchalgorithmus aus dem Makro FIND(A,V,I) zu verwenden, fügen wir einen überladenen Operator hinzu, der den Empfänger des Symbols mit der Zeichenkette vergleicht und „wahr“ zurückgibt, wenn das Instanzsymbol mit der übergebenen Zeichenkette übereinstimmt:

class CVirtualSymbolReceiver : public CReceiver {
   ...

public:
   ...
   bool              operator==(const string symbol) {// Operator for comparing by a symbol name
      return m_symbol == symbol;
   }
   ...
};

Hier finden Sie eine vollständige Beschreibung der Klasse CVirtualSymbolReceiver. Die spezifische Implementierung aller Methoden finden Sie in den beigefügten Dateien.

class CVirtualSymbolReceiver : public CReceiver {
   string            m_symbol;         // Symbol
   CVirtualOrder     *m_orders[];      // Array of open virtual positions
   bool              m_isChanged;      // Are there any changes in the composition of virtual positions?

   bool              m_isNetting;      // Is this a netting account?

   double            m_minMargin;      // Minimum margin for opening

   CPositionInfo     m_position;       // Object for obtaining properties of market positions
   CSymbolInfo       m_symbolInfo;     // Object for getting symbol properties
   CTrade            m_trade;          // Object for performing trading operations

   double            MarketVolume();   // Volume of open market positions
   double            VirtualVolume();  // Volume of open virtual positions
   bool              IsTradeAllowed(); // Is trading by symbol available? 

   // Required volume difference
   double            DiffVolume(double marketVolume, double virtualVolume);

   // Volume correction for the required difference
   bool              Correct(double oldVolume, double diffVolume);

   // Auxiliary opening methods
   bool              ClearOpen(double diffVolume);
   bool              AddBuy(double volume);
   bool              AddSell(double volume);
   
   // Auxiliary closing methods
   bool              CloseBuyPartial(double volume);
   bool              CloseSellPartial(double volume);
   bool              CloseHedgingPartial(double volume, ENUM_POSITION_TYPE type);
   bool              CloseFull();

   // Check margin requirements
   bool              FreeMarginCheck(double volume, ENUM_ORDER_TYPE type);

public:
                     CVirtualSymbolReceiver(ulong p_magic, string p_symbol);  // Constructor
   bool              operator==(const string symbol) {// Operator for comparing by a symbol name
      return m_symbol == symbol;
   }
   void              Open(CVirtualOrder *p_order);    // Register opening a virtual position
   void              Close(CVirtualOrder *p_order);   // Register closing a virtual position 
   
   virtual bool      Correct() override;              // Adjustment of open volumes
};

Wir speichern diesen Code in der Datei VirtualSymbolReceiver.mqh im aktuellen Ordner.


Vergleich der Ergebnisse

Die sich daraus ergebende Betriebsart kann wie folgt dargestellt werden:


Abb. 3. Betriebsart aus dem aktuellen Artikel

Abb. 3. Betriebsart aus dem aktuellen Artikel

Jetzt kommt der interessanteste Teil. Lassen Sie uns den EA kompilieren, der neun Instanzen von Strategien mit den gleichen Parametern wie im vorherigen Artikel verwendet. Führen wir Testläufe mit einem ähnlichen EA aus dem vorherigen Artikel und dem, den wir gerade zusammengestellt haben, durch:


Abb. 4. Ergebnisse des EA aus dem vorherigen Artikel


Abb. 5. Ergebnisse der EA aus dem aktuellen Artikel

Im Allgemeinen sind die Ergebnisse nahezu identisch. Die Saldenkurven sind im Allgemeinen ununterscheidbar. Kleine Unterschiede, die in den Berichten sichtbar werden, können auf verschiedene Gründe zurückzuführen sein und werden weiter analysiert.


Bewertung des weiteren Potenzials

In der Diskussion des vorangegangenen Artikels wurde eine logische Frage gestellt: Welches sind die attraktivsten Handelsergebnisse, die mit dem fraglichen Ansatz erzielt werden können? Bisher haben die Grafiken eine Rendite von 20 % über 5 Jahre ergeben, was nicht besonders attraktiv erscheint.

Die Antwort auf diese Frage kann vorerst wie folgt lauten. Erstens müssen die Ergebnisse, die auf die gewählten einfachen Strategien zurückzuführen sind, und die Ergebnisse, die sich aus der Umsetzung der gemeinsamen Arbeit ergeben, klar voneinander getrennt werden.

Die Ergebnisse der ersten Kategorie werden sich ändern, wenn eine einfache Strategie durch eine andere ersetzt wird. Es liegt auf der Hand, dass das Gesamtergebnis umso besser ausfällt, je besser die Ergebnisse der einzelnen Instanzen einfacher Strategien sind. Die hier vorgestellten Ergebnisse wurden mit einer Handelsidee erzielt, die zunächst genau durch ihre Qualität und Eignung bestimmt wurde. Wir bewerten diese Ergebnisse einfach anhand des Verhältnisses zwischen Gewinn und Verlust für das Testintervall.

Die Ergebnisse der zweiten Kategorie sind die vergleichenden Ergebnisse der gemeinsamen und der Einzelarbeit. Hier wird die Bewertung auf der Grundlage anderer Parameter vorgenommen: Verbesserung der Linearität der Wachstumskurve des Eigenkapitals, Verringerung des Drawdowns und andere. Diese Ergebnisse scheinen wichtiger zu sein, denn es besteht die Hoffnung, mit ihrer Hilfe die nicht besonders hervorragenden Ergebnisse der ersten Kategorie auf ein akzeptables Niveau zu bringen.

Für alle Ergebnisse ist es jedoch ratsam, zunächst den Handel mit variablen Lots einzuführen. Andernfalls ist es schwieriger, das Verhältnis zwischen Rentabilität und Ausfall auf der Grundlage von Testergebnissen abzuschätzen, obwohl dies immer noch möglich ist. 

Versuchen wir, eine kleine Ersteinlage zu nehmen und einen neuen optimalen Wert für die Größe der offenen Positionen für den maximal zulässigen Drawdown von 50% für einen Zeitraum von 5 Jahren (2018.01.01 — 2023.01.01) zu wählen. Nachfolgend finden Sie die Ergebnisse der EA-Läufe aus diesem Artikel mit einem anderen Positionsgrößenmultiplikator, aber konstant für alle fünf Jahre mit einer anfänglichen Einlage von 1000 USD. Im vorangegangenen Artikel wurden die Positionsgrößen auf die Einlagengröße von 10.000 USD kalibriert, sodass der anfängliche Wert von depoPart_ um etwa das Zehnfache reduziert wurde.


Abb. 6. Testergebnisse mit verschiedenen Positionsgrößen

Wir sehen, dass der EA bei minimalem depoPart_= 0,04 keine echten Positionen eröffnet hat, da ihr Volumen bei der Neuberechnung im Verhältnis zum Saldo weniger als 0,01 beträgt. Aber ab dem nächsten Multiplikatorwert depoPart_= 0,06 wurden Marktpositionen eröffnet.

Bei einem maximalen depoPart_ = 0,4 ergibt sich ein Gewinn von rund 22.800 USD. Der hier gezeigte Drawdown ist jedoch der relative Drawdown, der während der gesamten Laufzeit auftritt. Aber 10% von 23.000 und 1000 sind sehr unterschiedliche Werte. Daher sollten wir uns unbedingt die Ergebnisse eines einzelnen Laufs ansehen:



Abb. 7. Testergebnisse bei maximalem depoPart_ = 0,4

Wie Sie sehen können, wurde der Drawdown von 1167 USD tatsächlich erreicht, was zum Zeitpunkt des Erreichens nur 9,99% des aktuellen Guthabens ausmachte, aber wenn der Beginn des Testzeitraums unmittelbar vor diesem unangenehmen Moment gelegen hätte, dann hätten wir die gesamte Einlage verloren. Daher können wir diese Positionsgröße nicht verwenden.

Schauen wir uns die Ergebnisse an, wenn depoPart_ = 0,2



Abb. 8. Testergebnisse bei depoPart_ = 0,2


In diesem Fall überstieg der maximale Drawdown nicht 494 USD, d. h. etwa 50 % der ursprünglichen Einlage von 1000 USD. Bei einer solchen Positionsgröße kommt es also selbst dann nicht zum Verlust der gesamten Einlage, wenn der Beginn des Zeitraums während der betrachteten fünf Jahre so schlecht wie möglich gewählt wird.

Bei dieser Positionsgröße werden die Testergebnisse für 1 Jahr (2022) wie folgt aussehen:



Abb. 9. Testergebnisse für 2022 bei depoPart_ = 0,2

Bei einem erwarteten maximalen Drawdown von etwa 50 % ergibt sich also ein Gewinn von etwa 150 % pro Jahr.

Diese Ergebnisse sehen ermutigend aus, aber es gibt einen Haken an der Sache. Die Ergebnisse für das Jahr 2023, das nicht in die Parameteroptimierung einbezogen wurde, fallen deutlich schlechter aus:



Abb. 10. Testergebnisse für 2023 bei depoPart_ = 0,2

Natürlich haben wir am Ende des Jahres die 40 % Gewinn in den Testergebnissen erhalten, aber es gab kein nachhaltiges Wachstum in 8 von 12 Monaten. Dieses Problem scheint das Hauptproblem zu sein, und diese Artikelserie wird sich mit verschiedenen Lösungsansätzen befassen.


Schlussfolgerung

In diesem Artikel haben wir uns auf die weitere Entwicklung des Codes vorbereitet, indem wir den Code aus dem vorherigen Teil vereinfacht und optimiert haben. Wir haben einige zuvor festgestellte Mängel behoben, die unsere Fähigkeit, verschiedene Handelsstrategien zu nutzen, einschränken könnten. Die Testergebnisse zeigten, dass die neue Implementierung nicht schlechter funktioniert als die vorherige. Die Geschwindigkeit der Umsetzung blieb unverändert, aber es ist möglich, dass der Zuwachs erst bei einer mehrfachen Erhöhung der Anzahl der Strategieinstanzen auftritt.

Dazu müssen wir endlich herausfinden, wie wir die Eingabeparameter der Strategien speichern, wie wir sie in Parameterbibliotheken kombinieren und wie wir die besten Kombinationen aus denjenigen auswählen, die sich als Ergebnis der Optimierung einzelner Strategieinstanzen ergeben.

Wir werden die Arbeit in der gewählten Richtung im nächsten Artikel fortsetzen.



Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/14148

Verwendung von Optimierungsalgorithmen zur Konfiguration von EA-Parametern im laufenden Betrieb Verwendung von Optimierungsalgorithmen zur Konfiguration von EA-Parametern im laufenden Betrieb
Der Artikel behandelt die praktischen Aspekte der Verwendung von Optimierungsalgorithmen, um die besten EA-Parameter im laufenden Betrieb zu finden, sowie die Virtualisierung von Handelsoperationen und EA-Logik. Der Artikel kann als Anleitung für die Implementierung von Optimierungsalgorithmen in einen EA verwendet werden.
Algorithmen zur Optimierung mit Populationen: Künstliche multisoziale Suchobjekte (MSO) Algorithmen zur Optimierung mit Populationen: Künstliche multisoziale Suchobjekte (MSO)
Dies ist eine Fortsetzung des vorangegangenen Artikels, der sich mit dem Konzept der sozialen Gruppen befasst. In dem Artikel wird die Entwicklung sozialer Gruppen anhand von Bewegungs- und Gedächtnisalgorithmen untersucht. Die Ergebnisse werden dazu beitragen, die Entwicklung sozialer Systeme zu verstehen und sie bei der Optimierung und Suche nach Lösungen anzuwenden.
Risikobalance beim gleichzeitigen Handel von mehreren Handelsinstrumenten Risikobalance beim gleichzeitigen Handel von mehreren Handelsinstrumenten
Dieser Artikel ermöglicht es Anfängern, ein Skript für den Risikoausgleich beim gleichzeitigen Handel von mehreren Handelsinstrumenten von Grund auf zu schreiben. Darüber hinaus können erfahrene Nutzer neue Ideen für die Umsetzung ihrer Lösungen in Bezug auf die in diesem Artikel vorgeschlagenen Optionen erhalten.
Wie Sie mit der Erfüllung von Händleraufträgen im Freelance-Service Geld verdienen können Wie Sie mit der Erfüllung von Händleraufträgen im Freelance-Service Geld verdienen können
MQL5 Freelance ist ein Online-Dienst, bei dem Entwickler für die Erstellung von Handelsanwendungen für Händler als Kunden bezahlt werden. Der Dienst existiert seit 2010 sehr erfolgreich und hat bis heute über 100.000 Projekte im Gesamtwert von 7 Millionen Dollar abgeschlossen. Wie wir sehen, geht es hier um eine beträchtliche Menge Geld.