English Русский 中文 Español 日本語 Português
preview
Entwicklung eines Expertenberaters für mehrere Währungen (Teil 12): Entwicklung eines Risikomanagers auf der Ebene des Eigenhandels

Entwicklung eines Expertenberaters für mehrere Währungen (Teil 12): Entwicklung eines Risikomanagers auf der Ebene des Eigenhandels

MetaTrader 5Handel | 6 November 2024, 10:48
147 0
Yuriy Bykov
Yuriy Bykov

Einführung

In der gesamten Serie haben wir mehrfach das Thema Risikokontrolle angesprochen. Es wurden die Konzepte einer normalisierten Handelsstrategie vorgestellt, deren Parameter sicherstellen, dass während des Testzeitraums ein Drawdown-Niveau von 10% erreicht wird. Die Normalisierung von Handelsstrategie-Instanzen sowie von Gruppen von Handelsstrategien auf diese Weise kann jedoch nur einen bestimmten Drawdown über einen historischen Zeitraum liefern. Wir können nicht sicher sein, dass das angegebene Drawdown-Niveau eingehalten wird, wenn wir einen Test einer normalisierten Gruppe von Strategien in der Forward-Periode starten oder sie auf einem Handelskonto einführen.

Kürzlich wurde das Thema Risikomanagement in den Artikeln Risikomanager für manuellen Handel und Risikomanager für algorithmischen Handel behandelt. In diesen Artikeln schlug der Autor eine programmatische Implementierung vor, die die Einhaltung verschiedener Handelsparameter mit vorher festgelegten Indikatoren kontrolliert. Wird zum Beispiel die festgelegte Verlustgrenze für einen Tag, eine Woche oder einen Monat überschritten, wird der Handel ausgesetzt.

Der Artikel Einige Lektionen der Prop-Firmen erwies sich ebenfalls als interessant. Der Autor untersucht die Handelsanforderungen, die von Prop-Trading-Unternehmen auferlegt werden, um Händler herauszufordern, das Kapital für das Management zu erhalten. Trotz der zweideutigen Haltung gegenüber den Aktivitäten solcher Unternehmen, die auf verschiedenen dem Handel gewidmeten Ressourcen zu finden ist, ist die Anwendung klarer Regeln für das Risikomanagement eine der wichtigsten Komponenten eines erfolgreichen Handels. Es wäre also sinnvoll, die bereits gesammelten Erfahrungen zu nutzen und einen eigenen Risikomanager zu implementieren, der das Risikokontrollmodell der Prop-Trading-Unternehmen als Grundlage verwendet.


Modell und Konzepte

Für den Risikomanager sind die folgenden Konzepte von Nutzen:

  • Basissaldo — der anfänglicher Kontosaldo (oder Teil des Kontosaldos), aus dem die Werte der übrigen Parameter berechnet werden können. Wir verwenden hier den Wert 10.000.
  • Täglicher Basissaldo — der Saldo des Handelskontos zu Beginn des aktuellen Tageszeitraums. Der Einfachheit halber gehen wir davon aus, dass der Beginn der täglichen Periode mit dem Erscheinen eines neuen Balkens im Terminal auf dem Zeitrahmen D1 zusammenfällt.
  • Tägliches Basisguthaben — es ist das Guthaben auf dem Handelskonto zu Beginn der aktuellen Tagesperiode.
  • Tagesniveau — das ist das Maximum des täglichen Basisguthabens und der Geldmittel. Er wird zu Beginn der Tagesperiode festgelegt und behält seinen Wert für den Beginn der nächsten Tagesperiode.
  • Maximaler Tagesverlust — die Höhe der Abweichung des Kontoguthabens vom Tagesniveau nach unten, ab der der Handel in der laufenden Tagesperiode eingestellt werden soll. Der Handel wird in der nächsten Tagesperiode wieder aufgenommen. Ein Stop kann als verschiedene Aktionen verstanden werden, die darauf abzielen, den Umfang offener Positionen bis hin zur vollständigen Schließung zu reduzieren. Für den Anfang werden wir genau dieses einfache Modell verwenden: Wenn der maximale Tagesverlust erreicht ist, werden alle offenen Marktpositionen geschlossen. 
  • Maximaler Gesamtverlust — die Abweichung des Kontoguthabens vom Wert des Basissaldos nach unten, bei der der Handel vollständig eingestellt wird (er wird in den folgenden Perioden nicht wieder aufgenommen). Wenn dieses Niveau erreicht ist, werden alle offenen Positionen geschlossen.

Wir beschränken uns bei der Einstellung des Handels auf nur zwei Ebenen: täglich und insgesamt. Auf ähnliche Weise kann auch eine wöchentliche oder monatliche Ebene hinzugefügt werden. Da die Prop Trading-Unternehmen jedoch nicht über diese verfügen, werden wir die erste Implementierung unseres Risikomanagers nicht erschweren. Sie können bei Bedarf später hinzugefügt werden.

Verschiedene Prop-Trading-Unternehmen haben möglicherweise leicht unterschiedliche Ansätze zur Berechnung des maximalen Tages- und Gesamtverlustes. Deshalb bieten wir in unserem Risikomanager drei Möglichkeiten an, einen Zahlenwert für die Berechnung des maximalen Verlustes festzulegen:

  • Festgelegt in der Kontowährung. Hier übergeben wir den Verlustwert direkt in den Parameter, ausgedrückt in Einheiten der Währung des Handelskontos. Wir setzen ihn auf eine positive Zahl.
  • Als Prozentsatz des Basissaldos. In diesem Fall wird der Wert als Prozentsatz des festgelegten Basissaldos wahrgenommen. Da der Basissaldo in unserem Modell ein konstanter Wert ist (bis das Konto und der EA mit einem manuell eingestellten anderen Basissaldenwert neu gestartet werden), ist auch der auf diese Weise berechnete maximale Verlust ein konstanter Wert. Es wäre möglich, diesen Fall auf den ersten Fall zu reduzieren, aber da in der Regel der Prozentsatz des maximalen Verlustes angegeben wird, belassen wir ihn als separaten Fall.
  • In Prozent des Tageswertes. Bei dieser Option wird zu Beginn jeder Tagesperiode die maximale Verlusthöhe als ein bestimmter Prozentsatz der soeben berechneten Tageshöhe neu berechnet. Je höher das Guthaben oder die Mittel sind, desto größer ist auch der maximale Verlust. Diese Methode wird hauptsächlich zur Berechnung des maximalen Tagesverlustes verwendet. Der maximale Gesamtverlust wird in der Regel im Verhältnis zum Basissaldo festgelegt.

Beginnen wir mit der Implementierung unserer Risikomanager-Klasse, wie immer nach dem Prinzip des geringsten Eingriffs. Lassen Sie uns zunächst die minimal notwendige Umsetzung vornehmen und dabei die Möglichkeit einer eventuellen weiteren Verkomplizierung vorsehen.


Die Klasse CVirtualRiskManager

Die Entwicklung dieser Klasse durchlief mehrere Phasen. Zunächst wurde es als völlig statisches Objekt erstellt, damit es von allen Objekten frei verwendet werden konnte. Dann kam die Idee auf, dass wir auch die Parameter des Risikomanagers optimieren könnten, und es wäre schön, wenn wir sie als Initialisierungsstring speichern könnten. Zu diesem Zweck wurde die Klasse von der Klasse CFactorable abgeleitet. Das Singleton-Muster wurde implementiert, um sicherzustellen, dass der Risikomanager in Objekten verschiedener Klassen verwendet werden kann. Doch dann stellte sich heraus, dass der Risikomanager nur in einer einzigen Klasse benötigt wird - der EA-Klasse CVirtualAdvisor. Daher haben wir die Implementierung des Singleton-Musters aus der Risikomanager-Klasse entfernt.

Erstellen wir zunächst Enumeration für mögliche Risikomanager-Zustände und mögliche Methoden zur Berechnung von Limits:

// Possible risk manager states
enum ENUM_RM_STATE {
   RM_STATE_OK,            // Limits are not exceeded 
   RM_STATE_DAILY_LOSS,    // Daily limit is exceeded
   RM_STATE_OVERALL_LOSS   // Overall limit is exceeded
};


// Possible methods for calculating limits
enum ENUM_RM_CALC_LIMIT {
   RM_CALC_LIMIT_FIXED,          // Fixed (USD)
   RM_CALC_LIMIT_FIXED_PERCENT,  // Fixed (% from Base Balance)
   RM_CALC_LIMIT_PERCENT         // Relative (% from Daily Level)
};


In der Beschreibung der Risikomanager-Klasse werden wir mehrere Eigenschaften haben, um die Eingaben zu speichern, die über die Initialisierungszeichenfolge an den Konstruktor übergeben werden. Wir werden auch Eigenschaften zum Speichern verschiedener Berechnungsmerkmale hinzufügen - aktueller Saldo, Geldmittel, Gewinn und andere. Lassen Sie uns einige Hilfsmethoden im geschützten Bereich deklarieren. Im offenen Abschnitt werden wir im Wesentlichen nur einen Konstruktor und eine Methode für die Behandlung jedes Ticks haben. Wir werden vorerst nur die Methoden zum Speichern/Laden und den Operator zur Stringkonvertierung erwähnen und die Implementierung später schreiben.

Dann wird die Klassenbeschreibung etwa so aussehen:

//+------------------------------------------------------------------+
//| Risk management class (risk manager)                             |
//+------------------------------------------------------------------+
class CVirtualRiskManager : public CFactorable {
protected:
// Main constructor parameters
   bool              m_isActive;             // Is the risk manager active?

   double            m_baseBalance;          // Base balance

   ENUM_RM_CALC_LIMIT m_calcDailyLossLimit;  // Method of calculating the maximum daily loss
   double            m_maxDailyLossLimit;    // Parameter of calculating the maximum daily loss

   ENUM_RM_CALC_LIMIT m_calcOverallLossLimit;// Method of calculating the total daily loss
   double            m_maxOverallLossLimit;  // Parameter of calculating the maximum total loss

// Current state
   ENUM_RM_STATE     m_state;

// Updated values
   double            m_balance;              // Current balance
   double            m_equity;               // Current equity
   double            m_profit;               // Current profit
   double            m_dailyProfit;          // Daily profit
   double            m_overallProfit;        // Total profit
   double            m_baseDailyBalance;     // Daily basic balance
   double            m_baseDailyEquity;      // Daily base balance
   double            m_baseDailyLevel;       // Daily base level
   double            m_virtualProfit;        // Profit of open virtual positions

// Managing the size of open positions
   double            m_prevDepoPart;         // Used part of the total balance

// Protected methods
   double            DailyLoss();            // Maximum daily loss
   double            OverallLoss();          // Maximum total loss

   void              UpdateProfit();         // Update current profit values
   void              UpdateBaseLevels();     // Updating daily base levels

   void              CheckLimits();          // Check for excess of permissible losses
   void              CheckDailyLimit();      // Check for excess of the permissible daily loss
   void              CheckOverallLimit();    // Check for excess of the permissible total loss

   double            VirtualProfit();        // Determine the real size of the virtual position

public:
                     CVirtualRiskManager(string p_params);     // Constructor

   virtual void      Tick();                 // Tick processing in risk manager 

   virtual bool      Load(const int f);      // Load status
   virtual bool      Save(const int f);      // Save status

   virtual string    operator~() override;   // Convert object to string
};


Der Konstruktor des Risikomanager-Objekts erwartet, dass die Initialisierungszeichenfolge sechs numerische Werte enthält, die nach der Umwandlung in die entsprechenden Datentypen den Haupteigenschaften des Objekts zugewiesen werden. Außerdem wird bei der Erstellung der Status auf normal gesetzt (die Grenzwerte werden nicht überschritten). Wenn das Objekt bei einem Neustart des EA in der Mitte des Tages neu erstellt wird, sollte der Status beim Laden der gespeicherten Informationen auf den Status zum Zeitpunkt der letzten Speicherung korrigiert werden. Das Gleiche gilt für die Festlegung des Anteils des Kontosaldos, der für den Handel bestimmt ist - der im Konstruktor festgelegte Wert kann beim Laden der gespeicherten Risikomanagerinformationen vordefiniert werden.

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualRiskManager::CVirtualRiskManager(string p_params) {
// Save the initialization string
   m_params = p_params;

// Read the initialization string and set the property values
   m_isActive = (bool) ReadLong(p_params);
   m_baseBalance = ReadDouble(p_params);
   m_calcDailyLossLimit = (ENUM_RM_CALC_LIMIT) ReadLong(p_params);
   m_maxDailyLossLimit = ReadDouble(p_params);
   m_calcOverallLossLimit = (ENUM_RM_CALC_LIMIT) ReadLong(p_params);
   m_maxOverallLossLimit = ReadDouble(p_params);

// Set the state: Limits are not exceeded
   m_state = RM_STATE_OK;

// Remember the share of the account balance allocated for trading
   m_prevDepoPart = CMoney::DepoPart();

// Update base daily levels
   UpdateBaseLevels();

// Adjust the base balance if it is not set
   if(m_baseBalance == 0) {
      m_baseBalance = m_balance;
   }
}


Der Risikomanager führt die Hauptarbeit bei jedem Tick in der Ereignisbehandlung aus. Dabei werden die Aktivitäten des Risikomanagers überprüft und, falls aktiv, die aktuellen Gewinnwerte und die täglichen Basiswerte aktualisiert sowie überprüft, ob die Verlustgrenzen überschritten wurden:

//+------------------------------------------------------------------+
//| Tick processing in the risk manager                              |
//+------------------------------------------------------------------+
void CVirtualRiskManager::Tick() {
// If the risk manager is inactive, exit
   if(!m_isActive) {
      return;
   }

// Update the current profit values
   UpdateProfit();

// If a new daily period has begun, then we update the base daily levels
   if(IsNewBar(Symbol(), PERIOD_D1)) {
      UpdateBaseLevels();
   }

// Check for exceeding loss limits
   CheckLimits();
}


Auf einen weiteren Punkt möchten wir gesondert hinweisen. Dank der entwickelten Struktur mit virtuellen Positionen, die der Empfänger des Handelsvolumens in reale Marktpositionen umwandelt, und einem Kapitalverwaltungsmodul, das es uns ermöglicht, den erforderlichen Skalierungsfaktor zwischen den Größen der virtuellen und realen Positionen festzulegen, können wir sehr einfach eine sichere Schließung von Marktpositionen implementieren, die die Handelslogik der Arbeitsstrategien nicht verletzt. Dazu setzen Sie einfach den Skalierungsfaktor im Kapitalmanagementmodul auf 0:

CMoney::DepoPart(0);               // Set the used portion of the total balance to 0


Wenn wir uns vorher das vorherige Verhältnis in der Eigenschaft m_prevDepoPart merken, können wir nach einem neuen Tag und der Aktualisierung des Tageslimits zuvor geschlossene reale Positionen wiederherstellen, indem wir dieses Verhältnis einfach auf seinen vorherigen Wert zurücksetzen: 

CMoney::DepoPart(m_prevDepoPart);  // Return the used portion of the total balance


Gleichzeitig können wir natürlich nicht im Voraus wissen, ob die Positionen zu einem schlechteren oder besseren Preis wiedereröffnet werden. Wir können jedoch sicher sein, dass das Hinzufügen des Risikomanagers nicht die Leistung aller Instanzen von Handelsstrategien beeinflusst hat.

Schauen wir uns nun die übrigen Methoden der Klasse Risikomanager an.

In der Methode UpdateProfits() aktualisieren wir die aktuellen Werte von Saldo, Geldmittel und Gewinn und berechnen den Tagesgewinn als Differenz zwischen dem aktuellen Geldmitteln und dem Tagesstand. Es ist zu beachten, dass dieser Wert nicht immer mit dem aktuellen Gewinn übereinstimmen wird. Die Differenz wird angezeigt, wenn seit Beginn des neuen Tageszeitraums bereits einige Geschäfte abgeschlossen wurden. Wir berechnen den Gesamtverlust als Differenz zwischen den aktuellen Mitteln und dem Basissaldo.

//+------------------------------------------------------------------+
//| Updating current profit values                                   |
//+------------------------------------------------------------------+
void CVirtualRiskManager::UpdateProfit() {
   m_equity = AccountInfoDouble(ACCOUNT_EQUITY);
   m_balance = AccountInfoDouble(ACCOUNT_BALANCE);
   m_profit = m_equity - m_balance;
   m_dailyProfit = m_equity - m_baseDailyLevel;
   m_overallProfit = m_equity - m_baseBalance;
   m_virtualProfit = VirtualProfit();

   if(IsNewBar(Symbol(), PERIOD_H1) && PositionsTotal() > 0) {
      PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f",
                  m_virtualProfit, m_profit, m_dailyProfit);
   }
}

Bei dieser Methode berechnen wir auch den so genannten aktuellen virtuellen Gewinn. Sie wird auf der Grundlage der offenen virtuellen Positionen berechnet. Wenn wir virtuelle Positionen offen lassen, wenn die Risikomanagement-Beschränkungen ausgelöst werden, können wir auch ohne reale offene Positionen jederzeit schätzen, wie hoch der ungefähre Gewinn wäre, wenn die vom Risikomanager geschlossenen realen Positionen offen blieben. Leider liefert dieser berechnete Parameter kein ganz genaues Ergebnis (mit einem Fehler von mehreren Prozent). Sie ist aber dennoch nützlich.

Mit der Methode VirtualProfit() wird der aktuelle, virtuelle Gewinn berechnet. In dieser Methode erhalten wir einen Zeiger auf das virtuelle Empfängerobjekt des Volumens, da wir die Gesamtzahl der virtuellen Positionen ermitteln und auf jede virtuelle Position zugreifen können müssen. Dann durchlaufen wir eine Schleife durch alle virtuellen Positionen und bitten unser Geldverwaltungsmodul, den virtuellen Gewinn jeder Position zu berechnen und ihn auf die aktuellen Handelsmittel abzustimmen:

//+------------------------------------------------------------------+
//| Determine the profit of open virtual positions                   |
//+------------------------------------------------------------------+
double CVirtualRiskManager::VirtualProfit() {
   // Access the receiver object
   CVirtualReceiver *m_receiver = CVirtualReceiver::Instance();
   
   double profit = 0;
   
   // Find the profit sum for all virtual positions
   FORI(m_receiver.OrdersTotal(), profit += CMoney::Profit(m_receiver.Order(i)));
   
   return profit;
}

Bei dieser Methode haben wir ein neues Makro FORI verwendet, auf das wir weiter unten eingehen werden.

Wenn ein neuer Tageszeitraum beginnt, werden der tägliche Basissaldo, die Mittel und die Höhe neu berechnet. Wir werden auch überprüfen, ob das tägliche Verlustlimit am Vortag erreicht wurde, dann müssen wir den Handel wiederherstellen und reale Positionen in Übereinstimmung mit den offenen virtuellen Positionen wieder öffnen. Die Methode UpdateBaseLevels() übernimmt dies:

//+------------------------------------------------------------------+
//| Update daily base levels                                         |
//+------------------------------------------------------------------+
void CVirtualRiskManager::UpdateBaseLevels() {
// Update balance, funds and base daily level
   m_baseDailyBalance = m_balance;
   m_baseDailyEquity = m_equity;
   m_baseDailyLevel = MathMax(m_baseDailyBalance, m_baseDailyEquity);

   PrintFormat(__FUNCTION__" | DAILY UPDATE: Balance = %.2f | Equity = %.2f | Level = %.2f",
               m_baseDailyBalance, m_baseDailyEquity, m_baseDailyLevel);

// If the daily loss level was reached earlier, then
   if(m_state == RM_STATE_DAILY_LOSS) {
      // Restore the status to normal:
      CMoney::DepoPart(m_prevDepoPart);         // Return the used portion of the total balance
      m_state = RM_STATE_OK;                    // Set the risk manager to normal
      CVirtualReceiver::Instance().Changed();   // Notify the recipient about changes

      PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f",
                  m_virtualProfit, m_profit, m_dailyProfit);
      PrintFormat(__FUNCTION__" | RESTORE: depoPart = %.2f",
                  m_prevDepoPart);
   }
}


Zur Berechnung der maximalen Verluste nach den in den Parametern angegebenen Methoden stehen zwei Methoden zur Verfügung: DailyLoss() und OverallLoss(). Ihre Umsetzung ist sehr ähnlich, die einzigen Unterschiede sind die numerischen und methodischen Parameter, die für die Berechnung verwendet werden:

//+------------------------------------------------------------------+
//| Maximum daily loss                                               |
//+------------------------------------------------------------------+
double CVirtualRiskManager::DailyLoss() {
   if(m_calcDailyLossLimit == RM_CALC_LIMIT_FIXED) {
      // To get a fixed value, just return it 
      return m_maxDailyLossLimit;
   } else if(m_calcDailyLossLimit == RM_CALC_LIMIT_FIXED_PERCENT) {
      // To get a given percentage of the base balance, calculate it 
      return m_baseBalance * m_maxDailyLossLimit / 100;
   } else { // if(m_calcDailyLossLimit == RM_CALC_LIMIT_PERCENT)
      // To get a specified percentage of the daily level, calculate it
      return m_baseDailyLevel * m_maxDailyLossLimit / 100;
   }
}

//+------------------------------------------------------------------+
//| Maximum total loss                                               |
//+------------------------------------------------------------------+
double CVirtualRiskManager::OverallLoss() {
   if(m_calcOverallLossLimit == RM_CALC_LIMIT_FIXED) {
      // To get a fixed value, just return it 
      return m_maxOverallLossLimit;
   } else if(m_calcOverallLossLimit == RM_CALC_LIMIT_FIXED_PERCENT) {
      // To get a given percentage of the base balance, calculate it 
      return m_baseBalance * m_maxOverallLossLimit / 100;
   } else { // if(m_calcDailyLossLimit == RM_CALC_LIMIT_PERCENT)
      // To get a specified percentage of the daily level, calculate it
      return m_baseDailyLevel * m_maxOverallLossLimit / 100;
   }
}


Die Methode CheckLimits() zur Überprüfung der Limits ruft einfach zwei Hilfsmethoden auf, um den Tages- und Gesamtverlust zu überprüfen:

//+------------------------------------------------------------------+
//| Check loss limits                                                |
//+------------------------------------------------------------------+
void CVirtualRiskManager::CheckLimits() {
   CheckDailyLimit();      // Check daily limit
   CheckOverallLimit();    // Check total limit
}


Die Methode der täglichen Verlustkontrolle verwendet die Methode DailyLoss(), um die maximal zulässige tägliche Verlustgrenze zu ermitteln und vergleicht sie mit dem aktuellen Tagesgewinn. Bei Überschreitung des Limits geht der Risikomanager in den Zustand „Tageslimit überschritten“ über, und die Schließung offener Positionen wird eingeleitet, indem die Größe des verwendeten Handelssaldos auf 0 gesetzt wird:

//+------------------------------------------------------------------+
//| Check daily loss limit                                           |
//+------------------------------------------------------------------+
void CVirtualRiskManager::CheckDailyLimit() {
// If daily loss is reached and positions are still open
   if(m_dailyProfit < -DailyLoss() && CMoney::DepoPart() > 0) {
   // Switch the risk manager to the achieved daily loss state:
      m_prevDepoPart = CMoney::DepoPart();   // Save the previous value of the used part of the total balance
      CMoney::DepoPart(0);                   // Set the used portion of the total balance to 0
      m_state = RM_STATE_DAILY_LOSS;         // Set the risk manager to the achieved daily loss state
      CVirtualReceiver::Instance().Changed();// Notify the recipient about changes

      PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f",
                  m_virtualProfit, m_profit, m_dailyProfit);
      PrintFormat(__FUNCTION__" | RESET: depoPart = %.2f",
                  CMoney::DepoPart());
   }
}


Die Methode des Totalverlusttests funktioniert ähnlich, mit dem einzigen Unterschied, dass sie den Gesamtgewinn mit dem akzeptablen Gesamtverlust vergleicht. Wird das Gesamtlimit überschritten, geht der Risikomanager in den Zustand „Gesamtlimit überschritten“ über.

Speichern Sie den erhaltenen Code in der Datei VirtualRiskManager.mqh im aktuellen Ordner.

Schauen wir uns nun die Änderungen und Ergänzungen an, die wir an den zuvor erstellten Projektdateien vornehmen müssen, um unsere neue Risikomanager-Klasse verwenden zu können.


Nützliche Makros

Ich habe ein neues Makro FORI(N, D) zu der Liste der nützlichen Makros für die Arbeit mit Arrays hinzugefügt. Es wird eine Schleife mit der Variablen i eingerichtet, die N-mal den Ausdruck D ausführt:

// 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 FORI(N, D)      { for(int i=0; i<N;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

Speichern Sie die Änderungen in der Datei Macros.mqh des aktuellen Ordners.


Die Klasse für das Geldmanagement СMoney

In dieser Klasse werden wir eine Methode zur Berechnung des Gewinns einer virtuellen Position hinzufügen, die den Skalierungsfaktor ihres Volumens berücksichtigt. Wir führen eine ähnliche Operation in der Methode Volume() durch, um die berechnete Größe einer virtuellen Position zu bestimmen: Auf der Grundlage der Informationen über die aktuell verfügbare Bilanzgröße für den Handel und die Bilanzgröße, die dem Volumen der virtuellen Position entspricht, finden wir einen Skalierungsfaktor, der dem Verhältnis dieser Bilanzen entspricht. Dieser Faktor wird dann mit dem Volumen der virtuellen Position multipliziert, um das berechnete Volumen zu erhalten, d.h. das Volumen, das auf dem Handelskonto eröffnet wird.

Daher sollten wir zunächst den Teil des Codes, der den Skalierungsfaktor ermittelt, aus der Methode Volume() herausnehmen und in eine separate Methode Coeff() übertragen:

//+------------------------------------------------------------------+
//| Calculate the virtual position volume scaling factor             |
//+------------------------------------------------------------------+
double CMoney::Coeff(CVirtualOrder *p_order) {
   // Request the normalized strategy balance for the virtual position
   double fittedBalance = p_order.FittedBalance();

   // If it is 0, then the scaling factor is 1
   if(fittedBalance == 0.0) {
      return 1;
   }

   // Otherwise, find the value of the total balance for trading
   double totalBalance = s_fixedBalance > 0 ? s_fixedBalance : AccountInfoDouble(ACCOUNT_BALANCE);

   // Return the volume scaling factor
   return totalBalance * s_depoPart / fittedBalance;
}


Danach wird die Implementierung der Methoden Volume() und Profit() sehr ähnlich: Wir nehmen den gewünschten Wert (Volumen oder Gewinn) von der virtuellen Position und multiplizieren ihn mit dem resultierenden Skalierungsfaktor:

//+------------------------------------------------------------------+
//| Determine the calculated size of the virtual position            |
//+------------------------------------------------------------------+
double CMoney::Volume(CVirtualOrder *p_order) {
   return p_order.Volume() * Coeff(p_order);
}

//+------------------------------------------------------------------+
//| Determining the calculated profit of a virtual position          |
//+------------------------------------------------------------------+
double CMoney::Profit(CVirtualOrder *p_order) {
   return p_order.Profit() * Coeff(p_order);
}


Natürlich müssen wir der Klassenbeschreibung neue Methoden hinzufügen:

//+------------------------------------------------------------------+
//| Basic money management class                                     |
//+------------------------------------------------------------------+
class CMoney {
   ...
   
   // Calculate the scaling factor of the virtual position volume
   static double     Coeff(CVirtualOrder *p_order);

public:
   CMoney() = delete;                  // Disable the constructor
   
   // Determine the calculated size of the virtual position
   static double     Volume(CVirtualOrder *p_order);
   
   // Determine the calculated profit of a virtual position  
   static double     Profit(CVirtualOrder *p_order);  

   ...
};

Wir speichern die an der Datei Money.mqh vorgenommenen Änderungen im aktuellen Ordner.


Die Klasse СVirtualFactory

Da die von uns erstellte Risikomanagerklasse von der Klasse CFactorable abgeleitet ist, ist es notwendig, die Komposition der von CVirtualFactory erstellten Objekte zu erweitern, um die Möglichkeit ihrer Erstellung zu gewährleisten. Innerhalb der statischen Methode Create() fügen wir einen Codeblock hinzu, der für die Erstellung eines Objekts der Klasse CVirtualRiskManager verantwortlich ist:

//+------------------------------------------------------------------+
//| Object factory class                                             |
//+------------------------------------------------------------------+
class CVirtualFactory {
public:
   // Create an object from the initialization string
   static CFactorable* Create(string p_params) {
      // Read the object class name
      string className = CFactorable::ReadClassName(p_params);
      
      // Pointer to the object being created
      CFactorable* object = NULL;

      // Call the corresponding constructor  depending on the class name
      if(className == "CVirtualAdvisor") {
         object = new CVirtualAdvisor(p_params);
      } else if(className == "CVirtualRiskManager") {
         object = new CVirtualRiskManager(p_params);
      } else if(className == "CVirtualStrategyGroup") {
         object = new CVirtualStrategyGroup(p_params);
      } else if(className == "CSimpleVolumesStrategy") {
         object = new CSimpleVolumesStrategy(p_params);
      }
      
      ...

      return object;
   }
};

Den erhaltenen Code speichern wir in der Datei VirtualFactory.mqh im aktuellen Ordner.


Die Klasse CVirtualAdvisor

An der EA-Klasse CVirtualAdvisor müssen wir noch größere Änderungen vornehmen. Da wir beschlossen haben, dass das Risikomanager-Objekt nur innerhalb dieser Klasse verwendet werden soll, fügen wir der Klassenbeschreibung die entsprechende Eigenschaft hinzu:

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   CVirtualReceiver     *m_receiver;      // Receiver object that brings positions to the market
   CVirtualInterface    *m_interface;     // Interface object to show the status to the user
   CVirtualRiskManager  *m_riskManager;   // Risk manager object

   ...
};


Vereinbaren wir auch, dass der Initialisierungsstring des Risikomanagers in den EA-Initialisierungsstring unmittelbar nach dem Initialisierungsstring der Strategiegruppe eingebettet wird. Fügen wir noch hinzu, dass diese Initialisierungszeichenfolge im Konstruktor in die Variable riskManagerParams eingelesen und der Risikomanager anschließend daraus erstellt wird:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(string p_params) {
// Save the initialization string
   m_params = p_params;

// Read the initialization string of the strategy group object
   string groupParams = ReadObject(p_params);

// Read the initialization string of the risk manager object
   string riskManagerParams = ReadObject(p_params);

// Read the magic number
   ulong p_magic = ReadLong(p_params);

// Read the EA name
   string p_name = ReadString(p_params);

// Read the work flag only at the bar opening
   m_useOnlyNewBar = (bool) ReadLong(p_params);

// If there are no read errors,
   if(IsValid()) {
      ...
      
      // Create the risk manager object 
      m_riskManager = NEW(riskManagerParams);
   }
}


Da wir im Konstruktor ein Objekt erstellt haben, sollten wir auch dafür sorgen, dass es im Destruktor gelöscht wird:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   if(!!m_receiver)     delete m_receiver;      // Remove the recipient
   if(!!m_interface)    delete m_interface;     // Remove the interface
   if(!!m_riskManager)  delete m_riskManager;   // Remove risk manager
   DestroyNewBar();           // Remove the new bar tracking objects 
}


Das Wichtigste ist der Aufruf von Tick() für den Risikomanager des entsprechenden EAs. Bitte beachten Sie, dass der Risikomanager vor der Anpassung der Marktvolumina gestartet wird, sodass der Empfänger bei einer Überschreitung der Verlustlimits oder umgekehrt bei einer Aktualisierung der Limits die offenen Volumina der Marktpositionen bei der Bearbeitung desselben Ticks anpassen kann:

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Tick(void) {
// Define a new bar for all required symbols and timeframes
   bool isNewBar = UpdateNewBar();

// If there is no new bar anywhere, and we only work on new bars, then exit
   if(!isNewBar && m_useOnlyNewBar) {
      return;
   }

// Receiver handles virtual positions
   m_receiver.Tick();

// Start handling in strategies
   CAdvisor::Tick();

// Risk manager handles virtual positions
   m_riskManager.Tick();

// Adjusting market volumes
   m_receiver.Correct();

// Save status
   Save();

// Render the interface
   m_interface.Redraw();
}

Wir speichern die an der Datei VirtualAdvisor.mqh vorgenommenen Änderungen im aktuellen Ordner.


Der EA SimpleVolumesExpertSingle

Um den Risikomanager zu testen, muss nur noch die Möglichkeit hinzugefügt werden, seine Parameter im EA anzugeben und den erforderlichen Initialisierungsstring zu erzeugen. Lassen Sie uns zunächst alle sechs Parameter des Risikomanagers in separate EA-Eingänge verschieben:

input group "===  Risk management"
input bool        rmIsActive_             = true;
input double      rmStartBaseBalance_     = 10000;
input ENUM_RM_CALC_LIMIT 
                  rmCalcDailyLossLimit_   = RM_CALC_LIMIT_FIXED;
input double      rmMaxDailyLossLimit_    = 200;
input ENUM_RM_CALC_LIMIT 
                  rmCalcOverallLossLimit_ = RM_CALC_LIMIT_FIXED;
input double      rmMaxOverallLossLimit_  = 500;


In der Funktion OnInit() muss die Erstellung des Initialisierungsstrings für den Risikomanager hinzugefügt und in den EA-Initialisierungsstring eingebettet werden. Gleichzeitig werden wir den Code für die Erstellung von Initialisierungsstrings für eine Strategie und eine Gruppe, die diese eine Strategie enthält, leicht umschreiben, indem wir die Initialisierungsstrings der einzelnen Objekte in verschiedene Variablen aufteilen:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   CMoney::FixedBalance(fixedBalance_);
   CMoney::DepoPart(1.0);

// Prepare the initialization string for a single strategy instance
   string strategyParams = StringFormat(
                              "class CSimpleVolumesStrategy(\"%s\",%d,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d)",
                              Symbol(), Period(),
                              signalPeriod_, signalDeviation_, signaAddlDeviation_,
                              openDistance_, stopLevel_, takeLevel_, ordersExpiration_,
                              maxCountOfOrders_
                           );

// Prepare the initialization string for a group with one strategy instance
   string groupParams = StringFormat(
                           "class CVirtualStrategyGroup(\n"
                           "       [\n"
                           "        %s\n"
                           "       ],%f\n"
                           "    )",
                           strategyParams, scale_
                        );

// Prepare the initialization string for the risk manager
   string riskManagerParams = StringFormat(
                                 "class CVirtualRiskManager(\n"
                                 "       %d,%.2f,%d,%.2f,%d,%.2f"
                                 "    )",
                                 rmIsActive_, rmStartBaseBalance_,
                                 rmCalcDailyLossLimit_, rmMaxDailyLossLimit_,
                                 rmCalcOverallLossLimit_, rmMaxOverallLossLimit_
                              );

// Prepare the initialization string for an EA with a group of a single strategy and the risk manager
   string expertParams = StringFormat(
                            "class CVirtualAdvisor(\n"
                            "    %s,\n"
                            "    %s,\n"
                            "    %d,%s,%d\n"
                            ")",
                            groupParams,
                            riskManagerParams,
                            magic_, "SimpleVolumesSingle", true
                         );

   PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams);

// Create an EA handling virtual positions
   expert = NEW(expertParams);

   if(!expert) return INIT_FAILED;

   return(INIT_SUCCEEDED);
}

Den erhaltenen Code speichern wir in der Datei SimpleVolumesExpertSingle.mq5 im aktuellen Ordner. Jetzt ist alles bereit, um den Betrieb des Risikomanagers zu testen.


Test

Verwenden wir die Parameter einer der Handelsstrategien, die während der Optimierung in den vorangegangenen Phasen der Entwicklung ermittelt wurden. Wir bezeichnen dieses Beispiel einer Handelsstrategie als Modellstrategie. Die Parameter der Modellstrategie sind in Abb. 1 dargestellt.

Abb. 1. Parameter der Modellstrategie


Führen wir einen einzelnen Testdurchlauf mit diesen Parametern und ausgeschaltetem Risikomanager für den Zeitraum 2021-2022 durch. Wir erhalten die folgenden Ergebnisse:

Abb. 2. Modellstrategieergebnisse ohne den Risikomanager


Das Diagramm zeigt, dass es im ausgewählten Zeitraum zu mehreren merklichen Mittelabflüssen kam. Die größten davon traten Ende Oktober 2021 (~USD 380) und im Juni 2022 (~USD 840) auf.

Schalten wir nun den Risikomanager ein und setzen den maximalen Tagesverlust auf 150 USD und den maximalen Gesamtverlust auf 450 USD. Wir erhalten die folgenden Ergebnisse:


Abb. 3. Ergebnisse der Modellstrategie ohne den Risikomanager (maximale Verluste: 150 USD und 450 USD)


Die Grafik zeigt, dass der Risikomanager im Oktober 2021 zweimal verlustbringende Marktpositionen geschlossen hat, während virtuelle Positionen offen blieben. Deshalb wurden am nächsten Tag die Marktpositionen wieder geöffnet. Leider fand die Wiedereröffnung zu einem ungünstigeren Preis statt, sodass die Gesamtdrawdown des Guthabens und der Fonds die Inanspruchnahme des Eigenkapitals im Falle des behinderten Risikomanagers leicht überstieg. Es ist auch klar, dass die Strategie nach dem Schließen der Positionen statt eines kleinen Gewinns (wie es ohne den Risikomanager der Fall ist) einen Verlust erlitten hat.

Im Juni 2022 wurde der Risikomanager bereits sieben Mal ausgelöst und schloss Marktpositionen, wenn ein Tagesverlust von 150 USD erreicht wurde. Auch hier stellte sich heraus, dass die Wiedereröffnung zu ungünstigeren Preisen erfolgte, sodass durch diese Reihe von Transaktionen ein Verlust entstand. Aber wenn ein solcher EA auf einem Demokonto einer Prop-Trading-Firma mit solchen Parametern für maximale Tages- und Gesamtverluste arbeiten würde, dann würde das Konto ohne den Risikomanager wegen Verletzung der Handelsregeln gestoppt werden, und mit einem Risikomanager würde das Konto weiter arbeiten, mit einem etwas kleineren Gewinn als Ergebnis.

Obwohl ich den Gesamtverlust auf 450 USD festgesetzt habe und der Gesamtverlust im Juni 1000 USD überstieg, wurde der maximale Gesamtverlust nicht erreicht, da er vom Basissaldo berechnet wird. Mit anderen Worten: Sie ist erreicht, wenn die Mittel unter (10.000 - 450) = 9550 USD fallen. Aber aufgrund des zuvor angehäuften Gewinns sank der Betrag in diesem Zeitraum definitiv nicht unter 10.000 USD. Daher setzte die EA ihre Arbeit fort, begleitet von der Eröffnung von Marktpositionen.

Simulieren wir nun die Auslösung eines Totalverlustes. Dazu werden wir den Skalierungsfaktor der Positionsgrößen so erhöhen, dass im Oktober 2021 der maximale Gesamtverlust noch nicht überschritten wäre und im Juni 2022 die Überschreitung eintreten würde. Wir stellen scale_ = 50 ein und sehen uns das Ergebnis an:

Abb. 4. Ergebnisse der Modellstrategie ohne den Risikomanager (maximale Verluste: USD 150 und USD 450), scale_ = 50


Wie wir sehen können, endet der Handel im Juni 2022. In der Folgezeit hat die EA keine einzige Position eröffnet. Dies geschah aufgrund des Erreichens der Grenze für dem Gesamtverluste (9550 USD). Es ist auch festzustellen, dass der Tagesverlust nun häufiger erreicht wurde, und zwar nicht nur im Oktober 2021, sondern auch in mehreren anderen Zeiträumen.

Unsere beiden Begrenzer arbeiten also korrekt.

Der Risikomanager kann auch außerhalb von Prop-Trading-Unternehmen nützlich sein. Versuchen wir zur Veranschaulichung, die Parameter des Risikomanagers unserer Modellstrategie zu optimieren, indem wir versuchen, die Größe der eröffneten Positionen zu erhöhen, ohne jedoch den zulässigen Drawdown von 10 % zu überschreiten. Zu diesem Zweck wird in den Parametern des Risikomanagers der maximale Gesamtverlust auf 10 % des Tageswertes festgelegt. Wir werden auch den maximalen Tagesverlust, der ebenfalls als Prozentsatz des Tagesniveaus berechnet wird, während der Optimierung durchgehen.


Abb. 5. Ergebnisse der Modellstrategieoptimierung mit dem Risikomanager


Die Ergebnisse zeigen, dass der standardisierte Gewinn für ein Jahr durch den Einsatz des Risikomanagers fast um das Eineinhalbfache gestiegen ist: von 1560 USD auf 2276 USD (Spalte Ergebnis). So sieht der beste Durchgang aus, wenn er separat angezeigt wird:

Abb. 6. Ergebnisse der Modellstrategie ohne den Risikomanager (maximale Verluste: 7,6% und 10%, Skala_ = 88)


Beachten Sie, dass der EA während des gesamten Testzeitraums weiterhin Handelsgeschäfte eröffnete. Dies bedeutet, dass die Gesamtgrenze von 10 % nie verletzt wurde. Es macht natürlich keinen besonderen Sinn, einen Risikomanager auf einzelne Handelsstrategien anzuwenden, da wir nicht vorhaben, sie einzeln auf ein reales Konto zu übertragen. Was jedoch für eine Instanz funktioniert, sollte auch für einen EA mit vielen Instanzen gelten. Selbst diese kursorischen Ergebnisse erlauben uns also zu sagen, dass der Risikomanager durchaus nützlich sein kann.


    Schlussfolgerung

    Damit haben wir nun eine grundlegende Implementierung eines Risikomanagers für den Handel, der es uns ermöglicht, die festgelegten Höchstwerte für Tages- und Gesamtverluste einzuhalten. Er unterstützt noch nicht das Speichern und Laden des Status beim Neustart des EA, daher empfehle ich nicht, ihn auf einem echten Konto zu verwenden. Diese Änderung stellt jedoch keine besonderen Schwierigkeiten dar. Ich werde später darauf zurückkommen.

    Gleichzeitig kann versucht werden, den Handel nach verschiedenen Zeiträumen zu begrenzen, von der Sperrung des Handels zu bestimmten Stunden an bestimmten Wochentagen bis hin zum Verbot der Eröffnung neuer Positionen während wichtiger Wirtschaftsnachrichten. Andere mögliche Bereiche für die Entwicklung des Risikomanagers sind eine sanftere Veränderung der Positionsgröße (z. B. eine zweifache Reduzierung, wenn die Hälfte des Limits überschritten wird) und eine „intelligentere“ Wiederherstellung der Volumina (z. B. nur, wenn der Verlust ein Positionsreduktionsniveau überschreitet).

    Ich werde dies auf einen späteren Zeitpunkt verschieben. Im Moment werde ich mich wieder mit der Automatisierung der EA-Optimierung beschäftigen. Die erste Stufe wurde bereits im vorhergehenden Artikel umgesetzt. Es ist an der Zeit, zur zweiten Stufe überzugehen.

    Vielen Dank für Ihre Aufmerksamkeit! Bis bald!


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

    Beigefügte Dateien |
    Macros.mqh (2.4 KB)
    Money.mqh (6.52 KB)
    VirtualFactory.mqh (4.46 KB)
    VirtualAdvisor.mqh (23.02 KB)
    Visualisierung der Geschäfte auf dem Chart (Teil 1): Auswahl eines Zeitraums für die Analyse Visualisierung der Geschäfte auf dem Chart (Teil 1): Auswahl eines Zeitraums für die Analyse
    In diesem Artikel werden wir von Grund auf ein Skript zur einfachen Visualisierung von Handelsgeschäften (deals) für die nachträgliche Analyse von Handelsentscheidungen schreiben. Alle notwendigen Informationen über ein einzelnes Geschäft sollen bequem auf dem Chart angezeigt werden, wobei verschiedene Zeitrahmen gezeichnet werden können.
    Neuronales Netz in der Praxis: Geradenfunktion Neuronales Netz in der Praxis: Geradenfunktion
    In diesem Artikel werden wir einen kurzen Blick auf einige Methoden werfen, um eine Funktion zu erhalten, die unsere Daten in der Datenbank darstellen kann. Ich werde nicht im Detail darauf eingehen, wie man Statistiken und Wahrscheinlichkeitsstudien zur Interpretation der Ergebnisse verwendet. Überlassen wir das denjenigen, die sich wirklich mit der mathematischen Seite der Angelegenheit befassen wollen. Die Erforschung dieser Fragen wird entscheidend sein für das Verständnis dessen, was bei der Untersuchung neuronaler Netze eine Rolle spielt. Hier werden wir dieses Thema in aller Ruhe besprechen.
    Nachrichtenhandel leicht gemacht (Teil 2): Risikomanagement Nachrichtenhandel leicht gemacht (Teil 2): Risikomanagement
    In diesem Artikel wird die Vererbung in unseren bisherigen und neuen Code eingeführt. Um die Effizienz zu erhöhen, wird ein neues Datenbankdesign eingeführt. Darüber hinaus wird eine Risikomanagementklasse eingerichtet, die sich mit der Berechnung des Volumens befasst.
    Neuronale Netze leicht gemacht (Teil 89): Transformer zur Frequenzzerlegung (FEDformer) Neuronale Netze leicht gemacht (Teil 89): Transformer zur Frequenzzerlegung (FEDformer)
    Alle Modelle, die wir bisher betrachtet haben, analysieren den Zustand der Umwelt als Zeitfolge. Die Zeitreihen können aber auch in Form von Häufigkeitsmerkmalen dargestellt werden. In diesem Artikel stelle ich Ihnen einen Algorithmus vor, der Frequenzkomponenten einer Zeitsequenz zur Vorhersage zukünftiger Zustände verwendet.