English Русский 中文 Español 日本語 Português
preview
Entwicklung eines Expertenberaters für mehrere Währungen (Teil 14): Adaptive Volumenänderung im Risikomanager

Entwicklung eines Expertenberaters für mehrere Währungen (Teil 14): Adaptive Volumenänderung im Risikomanager

MetaTrader 5Handel | 31 Januar 2025, 11:46
82 0
Yuriy Bykov
Yuriy Bykov

Einführung

In einem der vorangegangenen Artikel dieser Reihe habe ich das Thema Risikokontrolle angesprochen und eine Risikomanager-Klasse entwickelt, die grundlegende Funktionen implementiert. Es ermöglicht die Festlegung eines maximalen Tagesverlusts und eines maximalen Gesamtverlusts, bei dessen Erreichen der Handel gestoppt und alle offenen Positionen geschlossen werden. Bei Erreichen eines Tagesverlustes wurde der Handel am nächsten Tag wieder aufgenommen, bei Erreichen eines Gesamtverlustes wurde er gar nicht mehr fortgesetzt.

Wie Sie sich vielleicht erinnern, waren die möglichen Bereiche für die Entwicklung des Risikomanagers 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). Wir können auch einen Parameter für den maximalen Zielgewinn hinzufügen, bei dessen Erreichen der Handel ebenfalls gestoppt wird. Dieser Parameter ist beim Handel auf einem Privatkonto wahrscheinlich nicht sinnvoll. Sie wird jedoch beim Handel auf den Konten von Unternehmen, die „Prop-Trading“ anbieten, sehr gefragt sein. Sobald die geplante Gewinnhöhe erreicht ist, kann der Handel in der Regel nur auf einem anderen Konto fortgesetzt werden.

Ich habe auch die Einführung von zeitbasierten Handelsbeschränkungen als eine der möglichen Richtungen für die Entwicklung eines Risikomanagers erwähnt, aber ich werde dieses Thema hier nicht behandeln und es der Zukunft überlassen. Versuchen wir zunächst, adaptive Volumenänderungen im Risikomanager zu implementieren und zu sehen, ob sich daraus ein Nutzen ergibt.


Basisfall

Verwenden wir den EA aus dem vorherigen Artikel. Wir werden ihm die Möglichkeit geben, die Parameter des Risikomanagers zu handhaben. Außerdem werden wir noch einige kleinere Ergänzungen vornehmen, die im Folgenden erläutert werden. Einigen wir uns zunächst auf die EA-Parameter, anhand derer wir die Ergebnisse der vorgenommenen Änderungen bewerten wollen.

Legen wir zunächst die spezifische Zusammensetzung der einzelnen Instanzen von Handelsstrategien fest, die im Test-EA verwendet werden sollen. Legen Sie die IDs der besten Durchläufe nach der zweiten Optimierungsstufe im Parameter passes_ für jedes der drei optimierten Symbole und jeden der drei Zeitrahmen fest (insgesamt 9 IDs). Hinter jeder ID verbirgt sich eine normalisierte Gruppe von 16 einzelnen Instanzen von Handelsstrategien. Die endgültige Gruppe wird also insgesamt 144 Instanzen von Handelsstrategien enthalten, die in 9 Gruppen zu je 16 Strategien aufgeteilt sind. Die letzte Gruppe wird nicht normalisiert, da wir für sie keinen Normalisierungsfaktor gewählt haben.

Zweitens verwenden wir ein festes Handelsguthaben von 10.000 USD und unseren standardmäßigen erwarteten maximalen Drawdown von 10 % für den Skalierungsfaktor von 1. Wir werden versuchen, letztere im Bereich von 1 bis 10 zu ändern. Gleichzeitig erhöht sich auch der maximal zulässige Drawdown, den wir nun aber zusätzlich mit unserem Risikomanager kontrollieren, damit er 10 % nicht übersteigt.

Dazu schalten wir den Risikomanager ein und setzen den maximalen Gesamtverlust auf 10 % des Basissaldos von 10.000 USD, also 1000 USD. Für den maximalen Tagesverlust setzen Sie den Wert doppelt so niedrig an, also 500 USD. Wenn der Saldo wächst, werden diese beiden Parameter unverändert bleiben.

Stellen wir alle Eingabewerte wie oben beschrieben ein:

Abb. 1. Die Eingaben des Test-EAs mit dem ursprünglichen Risikomanager


Führen wir die Optimierung für das Intervall 2021 und 2022 durch, um zu sehen, wie der EA mit verschiedenen Werten des Skalierungsmultiplikators für Positionsgrößen (scale_) funktioniert. Wir erhalten die folgenden Ergebnisse:

Abb. 2. Ergebnisse der Optimierung des Parameters scale_ im Test-EA mit dem ursprünglichen Risikomanager


Die Ergebnisse werden in aufsteigender Reihenfolge des scale_-Parameterwerts sortiert, d. h. je niedriger die Zeile, desto größer die Größe der vom EA verwendeten offenen Positionen. Es ist deutlich zu erkennen, dass ab einem bestimmten kritischen Wert das Endergebnis ein Verlust von etwas mehr als 1000 ist.

Während des Testintervalls kam es zu mehreren Drawdowns unterschiedlichen Ausmaßes. In den Passagen, in denen kein Drawdown das Aktienniveau unter 9000 USD fallen ließ, wurde der Handel bis zum Ende des Intervalls fortgesetzt. In den Durchgängen, in denen der Stand der Geldmittel während des Drawdowns unter 9000 USD fiel, wurde der Handel zu diesem Zeitpunkt eingestellt und nicht wieder aufgenommen. In diesen Passagen erlitten wir einen Verlust von rund 1000 USD. Die leichte Überschreitung des berechneten Wertes ist höchstwahrscheinlich darauf zurückzuführen, dass wir den Betriebsmodus des EA nur bei neuen Minutenbalken verwendet haben. Daher hatten die Kurse Zeit, sich innerhalb einer Minute von dem Moment an, in dem der festgelegte Verlust genau erreicht wurde, bis zu dem Moment, in dem der Risikomanager dies überprüfte und die Entscheidung traf, alle Positionen zu schließen, ein wenig mehr zu verändern.

Diese Unterschiede sind bei den meisten Läufen geringfügig, und wir können sie ignorieren, indem wir eine etwas niedrigere Grenze in den Parametern vereinbaren oder indem wir wie geplant die Arbeitsweise des Risikomanagers ändern. Der einzige besorgniserregende Durchgang ist der für scale_= 5,5, bei dem der Verlust nach Schließung aller Positionen den berechneten Verlust um mehr als 20 % überstieg und sich auf etwa 1234 USD belief.

Zur weiteren Analyse betrachten wir die Kurve des Saldos und des Kapitals für den allerersten Durchgang (scale_ = 1,0). Da sich die verbleibenden Durchgänge nur durch die Größe der eröffneten Positionen unterscheiden, sehen die Diagramme für den Saldo und das Kapital genauso aus wie im ersten Durchgang. Der einzige Unterschied besteht darin, dass sie vertikal stärker gestreckt werden.

Abb. 3. Bestehende Ergebnisse mit scale_ = 1,0 im Test-EA mit dem ursprünglichen Risikomanager


Vergleichen wir sie mit den Ergebnissen ohne den Risikomanager:

Abb. 4. Bestehende Ergebnisse mit scale_ = 1,0 im Test-EA ohne den Risikomanager


Ohne den Risikomanager war der Gesamtgewinn um einige Prozent höher, und der maximale Drawdown des Kapitals blieb gleich. Dies deutet darauf hin, dass die Normalisierung der Strategien bei ihrer Zusammenstellung sowie ihre anschließende gemeinsame Arbeit zu guten Ergebnissen führt: Der Risikomanager musste nur etwa dreimal in zwei Jahren Positionen schließen, wenn der Tagesverlust überschritten wurde.

Betrachten wir nun die Ergebnisse des Durchgangs mit dem Risikomanager und scale_ = 3. Der Gewinn war etwa 50 % höher, aber auch der Drawdown verdreifachte sich.

Abb. 5. Bestehende Ergebnisse mit scale_ = 3,0 im Test-EA mit dem ursprünglichen Risikomanager


Obwohl der Drawdown in absoluten Zahlen auf fast 3000 USD anstieg, musste der Risikomanager verhindern, dass der Drawdown 500 USD pro Tag überstieg. Mit anderen Worten, es gab mehrere aufeinanderfolgende Tage, an denen der Risikomanager alle Positionen schloss und sie zu Beginn des nächsten Tages wieder eröffnete. Vom vorherigen Höchstwert des Kapitals ging der aktuelle Betrag schließlich bis auf 3.000 USD zurück, aber der Drawdown überstieg nicht 500 USD für jeden einzelnen Tag, bezogen auf das maximale Kapital oder die maximalen Mittel zu Beginn des Tages. Die Verwendung einer solchen erhöhten Positionsgröße ist jedoch gefährlich, da es auch eine Begrenzung des Gesamtverlustes gibt. In diesem Fall hatten wir das Glück, dass der große Drawdown einige Zeit nach Beginn des Testzeitraums stattfand. Die Höhe des Saldos hatte Zeit zu wachsen und erhöhte somit den Wert des maximalen Gesamtverlusts, der zu Beginn 1000 USD betrug und vom Wert des Anfangssaldos abgezogen wurde. Hätte der Testzeitraum begonnen, kurz bevor der Drawdown 3000 USD erreichte, wäre das Gesamtlimit überschritten und der Handel gestoppt worden.

Um die Auswirkungen eines möglichen schlechten Startzeitpunkts zu berücksichtigen, werden wir die Parameter des Risikomanagers so ändern, dass die Höhe des Gesamtverlusts nicht aus dem Anfangssaldo, sondern aus dem letzten Maximum des Saldos oder des Kapitals berechnet wird. Dazu müssen wir jedoch zunächst den Code des Risikomanagers ergänzen, da die Möglichkeit, den gewünschten Parameterwert zu setzen, noch nicht implementiert ist.


CVirtualRiskManager aktualisieren

Wir haben einige Änderungen für die Klasse der Risikomanager geplant. Viele von ihnen erfordern Änderungen an denselben Methoden. Dies macht die Beschreibung etwas schwierig, da es schwierig ist, Änderungen, die sich auf verschiedene hinzugefügte Funktionen beziehen, zu trennen. Daher werden wir die bereits fertiggestellte Version des Codes beschreiben, beginnend mit der einfachsten Passage.


Enumeration aktualisieren

Vor der Klassenbeschreibung haben wir mehrere Enumerationen deklariert, die später verwendet werden sollen. Wir werden die Zusammensetzung dieser Enumerationen leicht erweitern. Zum Beispiel erhält die Enumeration ENUM_RM_STATE, die mögliche Zustände des Risikomanagers enthält, zwei neue Zustände:

  • RM_STATE_RESTORE — Zustand, der nach dem Beginn einer neuen Tagesperiode eintritt, bis zu dem Zeitpunkt, an dem die Größen der offenen Positionen vollständig wiederhergestellt sind. Zuvor trat diese Bedingung nicht auf, da wir die Positionsgrößen sofort nach Beginn eines neuen Tages wiederherstellten. Jetzt haben wir die Möglichkeit, dies nicht sofort zu tun, sondern erst dann, wenn die Preise wieder zu günstigeren Werten für die Eröffnung zurückkehren. Darauf werden wir später noch genauer eingehen.

  • RM_STATE_OVERALL_PROFIT — Zustand, der eintritt, nachdem der angegebene Gewinn erreicht wurde. Nach diesem Ereignis wird der Handel eingestellt.

Wir haben die bisherige Enumeration ENUM_RM_CALC_LIMIT in drei separate Enumerationen umgewandelt: für den Tagesverlust, den Gesamtverlust und den Gesamtgewinn. Die Werte dieser Enumerationen werden verwendet, um zwei Dinge zu bestimmen:

  • wie die in den Parametern übergebene Zahl zu verwenden ist: als absoluter oder als relativer Wert, der als Prozentsatz des Tagesstandes oder der Höchstwerte des Saldos oder des Kapitals angegeben wird;
  • von (bis) welchem Wert aus der Schwellenwert ermittelt werden soll — dem Tagesniveau, dem höchsten Saldo oder Kapital.

Diese Optionen sind in den Kommentaren zu den Enumerationswerten angegeben.

// 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_RESTORE,       // Recovery after daily limit
   RM_STATE_OVERALL_LOSS,  // Overall limit exceeded
   RM_STATE_OVERALL_PROFIT // Overall profit reached
};

// Possible methods for calculating limits
enum ENUM_RM_CALC_DAILY_LOSS {
   RM_CALC_DAILY_LOSS_MONEY_BB,    // [$] to Daily Level
   RM_CALC_DAILY_LOSS_PERCENT_BB,  // [%] from Base Balance to Daily Level
   RM_CALC_DAILY_LOSS_PERCENT_DL   // [%] from/to Daily Level
};

// Possible methods for calculating general limits
enum ENUM_RM_CALC_OVERALL_LOSS {
   RM_CALC_OVERALL_LOSS_MONEY_BB,           // [$] to Base Balance
   RM_CALC_OVERALL_LOSS_MONEY_HW_BAL,       // [$] to HW Balance
   RM_CALC_OVERALL_LOSS_MONEY_HW_EQ_BAL,    // [$] to HW Equity or Balance
   RM_CALC_OVERALL_LOSS_PERCENT_BB,         // [%] from/to Base Balance
   RM_CALC_OVERALL_LOSS_PERCENT_HW_BAL,     // [%] from/to HW Balance
   RM_CALC_OVERALL_LOSS_PERCENT_HW_EQ_BAL   // [%] from/to HW Equity or Balance
};

// Possible methods for calculating overall profit
enum ENUM_RM_CALC_OVERALL_PROFIT {
   RM_CALC_OVERALL_PROFIT_MONEY_BB,           // [$] to Base Balance
   RM_CALC_OVERALL_PROFIT_PERCENT_BB,         // [%] from/to Base Balance
};


Klasse Beschreibung

Wir haben neue Eigenschaften und Methoden im geschützten Bereich der Klassenbeschreibung vom CVirtualRiskManager hinzugefügt. Im öffentlichen Teil bleibt alles unverändert. Hinzugefügte Zeichenfolgen sind grün hervorgehoben:

//+------------------------------------------------------------------+
//| 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_DAILY_LOSS   m_calcDailyLossLimit; // Method of calculating the maximum daily loss
   double            m_maxDailyLossLimit;          // Parameter of calculating the maximum daily loss
   double            m_closeDailyPart;             // Threshold part of the daily loss

   ENUM_RM_CALC_OVERALL_LOSS m_calcOverallLossLimit;  // Method of calculating the maximum overall loss
   double            m_maxOverallLossLimit;           // Parameter of calculating the maximum overall loss
   double            m_closeOverallPart;              // Threshold part of the overall loss

   ENUM_RM_CALC_OVERALL_PROFIT m_calcOverallProfitLimit; // Method for calculating maximum overall profit
   double            m_maxOverallProfitLimit;            // Parameter for calculating the maximum overall profit

   double            m_maxRestoreTime;             // Waiting time for the best entry on a drawdown
   double            m_lastVirtualProfitFactor;    // Initial best drawdown multiplier


// Current state
   ENUM_RM_STATE     m_state;                // State
   double            m_lastVirtualProfit;    // Profit of open virtual positions at the moment of loss limit 
   datetime          m_startRestoreTime;     // Start time of restoring the size of open positions

// 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;        // Overall profit
   double            m_baseDailyBalance;     // Daily basic balance
   double            m_baseDailyEquity;      // Daily base balance
   double            m_baseDailyLevel;       // Daily base level
   double            m_baseHWBalance;        // balance High Watermark
   double            m_baseHWEquityBalance;  // equity or balance High Watermark
   double            m_virtualProfit;        // Profit of open virtual positions

// Managing the size of open positions
   double            m_baseDepoPart;         // Used (original) part of the overall balance
   double            m_dailyDepoPart;        // Multiplier of the used part of the overall balance by daily loss
   double            m_overallDepoPart;      // Multiplier of the used part of the overall balance by overall loss

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

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

   void              CheckLimits();          // Check for excess of permissible losses
   bool              CheckDailyLossLimit();     // Check for excess of the permissible daily loss
   bool              CheckOverallLossLimit();   // Check for excess of the permissible overall loss
   bool              CheckOverallProfitLimit(); // Check if the specified profit has been achieved

   void              CheckRestore();         // Check the need for restoring the size of open positions
   bool              CheckDailyRestore();       // Check if the daily multiplier needs to be restored
   bool              CheckOverallRestore();     // Check if the overall multiplier needs to be restored

   double            VirtualProfit();        // Determine the profit of open virtual positions
   double            RestoreVirtualProfit(); // Determine the profit of open virtual positions to restore

   void              SetDepoPart();          // Set the values of the used part of the overall balance

public:
   ...
};

Die Eigenschaften m_closeDailyPart und m_closeOverallPart ermöglichen es uns, die Größe der Positionen sanfter zu verändern. Ihre Verwendung ist ähnlich, und der einzige Unterschied besteht darin, auf welches Limit (täglich oder allgemein) sich jede Eigenschaft bezieht. Wenn wir z. B. m_closeDailyPart = 0,5 einstellen, werden die Positionsgrößen halbiert, wenn der Verlust die Hälfte des Tageslimits erreicht. Wenn der Verlust weiter wächst und die Hälfte der verbleibenden Hälfte des Tageslimits erreicht, werden die (bereits halbierten) Positionsgrößen erneut halbiert.

Die Verringerung der Größe der Positionen erfolgt durch Änderung der Eigenschaften m_dailyDepoPart und m_overallDepoPart. Ihre Werte werden in der Methode zur Festlegung des verwendeten Anteils der Gesamtbilanz für den Handel verwendet. Sie gehen als Multiplikatoren in die Gleichung ein, sodass die Halbierung eines dieser Faktoren zu einer Halbierung des Gesamtvolumens führt:

//+------------------------------------------------------------------+
//| Set the value of the used part of the overall balance            |
//+------------------------------------------------------------------+
void CVirtualRiskManager::SetDepoPart() {
   CMoney::DepoPart(m_baseDepoPart * m_dailyDepoPart * m_overallDepoPart);
}

Die in der Funktion verwendete Eigenschaft m_baseDepoPart enthält den Wert der ursprünglichen Größe des für den Handel verwendeten Teils der Bilanz.

Die Eigenschaften m_maxRestoreTime und m_lastVirtualProfitFactor werden verwendet, um die Möglichkeit der Wiederherstellung der Größe der offenen Positionen zu bestimmen.

Die erste Eigenschaft gibt die Zeit in Minuten an, nach der die Größen auch bei einem nicht negativen Wert des virtuellen Gewinns wiederhergestellt werden. Das heißt, nach dieser Zeit wird der EA wieder reale Marktpositionen eröffnen, die den virtuellen Positionen entsprechen, auch wenn in der Zeit, in der die realen Positionen geschlossen wurden, die Kurse in die richtige Richtung gingen und der virtuelle Gewinn größer wurde als beim Erreichen der Grenzen. Bis zu diesem Zeitpunkt wird das Volumen nur dann wiederhergestellt, wenn der geschätzte Gewinn der virtuellen Positionen unter einem geschätzten Wert liegt, der sich im Laufe der Zeit ändert.

Die zweite Eigenschaft gibt einen Multiplikator an, der bestimmt, wie oft der Verlust virtueller Positionen zu Beginn eines neuen Tageszeitraums größer sein soll als der Verlust virtueller Positionen zum Zeitpunkt des Erreichens der Limits, damit die Positionsgrößen sofort wiederhergestellt werden. Ein Wert von 1 für diesen Parameter würde z. B. bedeuten, dass die Wiederherstellung der Größen zu Beginn des Tages nur dann erfolgt, wenn die virtuellen Positionen im gleichen Drawdown verbleiben oder in einen noch tieferen Drawdown übergehen als im letzten Moment, in dem die Grenzen erreicht wurden.

Zu beachten ist auch, dass das Erreichen des Limits nun nicht nur dann als ausgelöst gilt, wenn der Drawdown das festgelegte Limit überschreitet, sondern beispielsweise auch, wenn er die Hälfte des Tageslimits erreicht, wenn m_closeDailyPart 0,5 ist.


Berechnung des Gewinns von virtuellen Positionen

Bei der Anwendung der teilweisen Schließung und der anschließenden Wiederherstellung der Größe der offenen Positionen muss korrekt ermittelt werden, wie hoch der schwebende Gewinn oder Verlust wäre, wenn alle gemäß der Strategie eröffneten Positionen weiterhin offen blieben. Daher wurde die Methode zur Berechnung des aktuellen Gewinns offener virtueller Positionen in die Klasse der Risikomanager aufgenommen. Sie werden nicht geschlossen, wenn die Drawdown-Grenzen erreicht sind, sodass wir jederzeit feststellen können, wie hoch der ungefähre Gewinn bei offenen Marktpositionen gewesen wäre, die den virtuellen Positionen entsprechen. Dieser berechnete Wert entspricht aufgrund der fehlenden Berücksichtigung von Provisionen und Swaps nicht genau der Realität, aber für unsere Zwecke ist diese Genauigkeit völlig ausreichend.

Beim letzten Mal brauchten wir diese Methode nicht für Berechnungen, deren Ergebnisse das weitere Vorgehen bestimmen würden. Jetzt brauchen wir sie. Außerdem sollten wir einige kleine Ergänzungen vornehmen, um den Gewinn von virtuellen Positionen korrekt zu berechnen. Der Punkt ist, dass die Methode zur Ermittlung des Gewinns einer virtuellen Position CMoney::Profit() den Multiplikator für die Beanspruchung des Saldos CMoney::DepoPart() zur Berechnung verwendet. Wenn wir diesen Multiplikator reduziert haben, dann wird die Berechnung des virtuellen Gewinns für die reduzierten Positionsgrößen vorgenommen und nicht für die Größen, die vor der Reduzierung vorhanden waren.

Da uns der Gewinn für die anfänglichen Positionsgrößen interessiert, geben wir vor seiner Berechnung vorübergehend den Multiplikator für die Nutzung des Anfangssaldos zurück. Danach berechnen wir den Gewinn der virtuellen Positionen und setzen dann den Multiplikator für den aktuellen Saldoeinsatz erneut, indem wir die Methode SetDepoPart() aufrufen:

//+------------------------------------------------------------------+
//| Determine the profit of open virtual positions                   |
//+------------------------------------------------------------------+
double CVirtualRiskManager::VirtualProfit() {
// Access the receiver object
   CVirtualReceiver *m_receiver = CVirtualReceiver::Instance();

// Set the initial balance usage multiplier
   CMoney::DepoPart(m_baseDepoPart);

   double profit = 0;

// Find the profit sum for all virtual positions
   FORI(m_receiver.OrdersTotal(), profit += CMoney::Profit(m_receiver.Order(i)));

// Restore the current balance usage multiplier
   SetDepoPart();

   return profit;
}


High Watermark (vorausgangener Höchstwert)

Zur besseren Analyse möchten wir die Möglichkeit hinzufügen, die maximale Verlusthöhe nicht nur aus dem anfänglichen Kontostand zu berechnen, sondern z.B. auch aus dem zuletzt erreichten Kontohöchststand. Daher haben wir m_baseHWBalance und m_baseHWEquityBalance in die Liste der Eigenschaften aufgenommen, die ständig aktualisiert werden sollten. In der Methode UpdateProfit() haben wir ihre Berechnung zusammen mit der Prüfung hinzugefügt, dass der Gesamtgewinn in Bezug auf die höchsten Werte des Saldos oder des Kapitals und nicht auf den Basissaldo berechnet wird:

//+------------------------------------------------------------------+
//| Updating current profit values                                   |
//+------------------------------------------------------------------+
void CVirtualRiskManager::UpdateProfit() {
// Current equity
   m_equity = AccountInfoDouble(ACCOUNT_EQUITY);

// Current balance
   m_balance = AccountInfoDouble(ACCOUNT_BALANCE);

// Maximum balance (High Watermark)
   m_baseHWBalance = MathMax(m_balance, m_baseHWBalance);

// Maximum balance or equity (High Watermark)
   m_baseHWEquityBalance = MathMax(m_equity, MathMax(m_balance, m_baseHWEquityBalance));

// Current profit
   m_profit = m_equity - m_balance;

// Current daily profit relative to the daily level
   m_dailyProfit = m_equity - m_baseDailyLevel;

// Current overall profit relative to base balance
   m_overallProfit = m_equity - m_baseBalance;

// If we take the overall profit relative to the highest balance,
   if(m_calcOverallLossLimit       == RM_CALC_OVERALL_LOSS_MONEY_HW_BAL
         || m_calcOverallLossLimit == RM_CALC_OVERALL_LOSS_PERCENT_HW_BAL) {
      // Recalculate it
      m_overallProfit = m_equity - m_baseHWBalance;
   }

// If we take the overall profit relative to the highest balance or equity,
   if(m_calcOverallLossLimit       == RM_CALC_OVERALL_LOSS_MONEY_HW_EQ_BAL
         || m_calcOverallLossLimit == RM_CALC_OVERALL_LOSS_PERCENT_HW_EQ_BAL) {
      // Recalculate it
      m_overallProfit = m_equity - m_baseHWEquityBalance;
   }

// Current profit of virtual open positions
   m_virtualProfit = VirtualProfit();

   ...
}


Methode der Grenzwertprüfung

Diese Methode wurde ebenfalls geändert: Wir haben ihren Code in mehrere Hilfsmethoden aufgeteilt, sodass sie jetzt wie folgt aussieht:

//+------------------------------------------------------------------+
//| Check loss limits                                                |
//+------------------------------------------------------------------+
void CVirtualRiskManager::CheckLimits() {
   if(false
         || CheckDailyLossLimit()     // Check daily limit
         || CheckOverallLossLimit()   // Check overall limit
         || CheckOverallProfitLimit() // Check overall profit
     ) {
      // Remember the current level of virtual profit
      m_lastVirtualProfit = m_virtualProfit;

      // Notify the recipient about changes
      CVirtualReceiver::Instance().Changed();
   }
}

In der Methode zur Überprüfung der Tagesverlustgrenze wird zunächst geprüft, ob die Tagesgrenze oder ein bestimmter Teil davon, definiert durch den Parameter m_closeDailyPart, erreicht wurde. Wenn ja, dann reduzieren wir den Multiplikator des verbrauchten Teils des Gesamtsaldos um den Tagesverlust. Wenn er bereits zu klein geworden ist, setzen wir ihn komplett zurück. Danach stellen wir den Wert des verbrauchten Teils des Gesamtsaldos ein und schalten den Risikomanager auf den Zustand des erreichten Tagesverlustes.

//+------------------------------------------------------------------+
//| Check daily loss limit                                           |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::CheckDailyLossLimit() {
// If daily loss is reached and positions are still open
   if(m_dailyProfit < -DailyLoss() * (1 - m_dailyDepoPart * (1 - m_closeDailyPart))
      && CMoney::DepoPart() > 0) {

      // Reduce the multiplier of the used part of the overall balance by the daily loss
      m_dailyDepoPart *= (1 - m_closeDailyPart);

      // If the multiplier is already too small,
      if(m_dailyDepoPart < 0.05) {
         // Set it to 0
         m_dailyDepoPart = 0;
      }

      // Set the value of the used part of the overall balance
      SetDepoPart();
    
      ... 

      // Set the risk manager to the achieved daily loss state
      m_state = RM_STATE_DAILY_LOSS;

      return true;
   }

   return false;
}

Die Methode zur Überprüfung der Gesamtverlustgrenze funktioniert auf ähnliche Weise. Der einzige Unterschied besteht darin, dass der Risikomanager nur dann in den Zustand des erreichten Gesamtschadens übergeht, wenn der Multiplikator des verwendeten Teils des Gesamtsaldos für den Gesamtschaden gleich Null geworden ist:

//+------------------------------------------------------------------+
//| Check the overall loss limit                                     |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::CheckOverallLossLimit() {
// If overall loss is reached and positions are still open
   if(m_overallProfit < -OverallLoss() * (1 - m_overallDepoPart * (1 - m_closeOverallPart))
         && CMoney::DepoPart() > 0) {
      // Reduce the multiplier of the used part of the overall balance by the overall loss
      m_overallDepoPart *= (1 - m_closeOverallPart);

      // If the multiplier is already too small,
      if(m_overallDepoPart < 0.05) {
         // Set it to 0
         m_overallDepoPart = 0;

         // Set the risk manager to the achieved overall loss state
         m_state = RM_STATE_OVERALL_LOSS;
      }

      // Set the value of the used part of the overall balance
      SetDepoPart();
      
      ...

      return true;
   }

   return false;
}

Die Überprüfung, ob der vorgegebene Gewinn erreicht wurde, sieht noch einfacher aus: Wenn er erreicht ist, setzen wir den entsprechenden Multiplikator zurück und setzen den Risikomanager auf den Stand des erreichten Gesamtgewinns:

//+------------------------------------------------------------------+
//| Check if the specified profit has been achieved                  |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::CheckOverallProfitLimit() {
// If overall loss is reached and positions are still open
   if(m_overallProfit > m_maxOverallProfitLimit && CMoney::DepoPart() > 0) {
      // Reduce the multiplier of the used part of the overall balance by the overall loss
      m_overallDepoPart = 0;

      // Set the risk manager to the achieved overall profit state
      m_state = RM_STATE_OVERALL_PROFIT;

      // Set the value of the used part of the overall balance
      SetDepoPart();
      
      ... 

      return true;
   }

   return false;
}

Alle drei Methoden geben einen booleschen Wert zurück, der die Frage beantwortet: Wurde das entsprechende Limit erreicht? Wenn ja, speichern wir die aktuelle Höhe des virtuellen Gewinns in der Methode CheckLimits() und benachrichtigen den Empfänger der Marktpositionsvolumina über Änderungen in der Zusammensetzung der Positionen.

Bei der Methode der täglichen Basislinienaktualisierung haben wir die Neuberechnung des täglichen Gewinns und den Wechsel in den Erholungszustand hinzugefügt, wenn die tägliche Verlustgrenze zuvor erreicht wurde:

//+------------------------------------------------------------------+
//| 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);

   m_dailyProfit = m_equity - m_baseDailyLevel;

   ...

// If the daily loss level was reached earlier, then
   if(m_state == RM_STATE_DAILY_LOSS) {
      // Switch to the state of restoring the sizes of open positions
      m_state = RM_STATE_RESTORE;

      // Remember restoration start time
      m_startRestoreTime = TimeCurrent();
   }
}


Wiederherstellung der Positionsgrößen

Wir haben auch die Methode, die bisher diese Aufgabe erfüllte, in mehrere Methoden aufgeteilt. Die Methode zur Prüfung, ob eine Wiederherstellung erforderlich ist, wird auf der obersten Ebene aufgerufen:

//+------------------------------------------------------------------+
//| Check the need for restoring the size of open positions          |
//+------------------------------------------------------------------+
void CVirtualRiskManager::CheckRestore() {
// If we need to restore the state to normal, then
   if(m_state == RM_STATE_RESTORE) {
      // Check the possibility of restoring the daily loss multiplier to normal 
      bool dailyRes = CheckDailyRestore();

      // Check the possibility of restoring the overall loss multiplier to normal
      bool overallRes = CheckOverallRestore();

      // If at least one of them has recovered,
      if(dailyRes || overallRes) {
         
...

         // Set the value of the used part of the overall balance
         SetDepoPart();

         // Notify the recipient about changes
         CVirtualReceiver::Instance().Changed();

         // If both multipliers are restored to normal,
         if(dailyRes && overallRes) {
            // Set normal state
            m_state = RM_STATE_OK;
         }
      }
   }
}

Die Hilfsmethoden zur Überprüfung, ob der Tages- und der Gesamtmultiplikator wiederhergestellt werden sollten, funktionieren derzeit auf die gleiche Weise, aber in Zukunft werden wir ihr Verhalten möglicherweise ändern. Im Moment wird geprüft, ob der aktuelle Wert des virtuellen Gewinns unter dem gewünschten Niveau liegt. Wenn dies der Fall ist, ist es für uns von Vorteil, reale Positionen zu den aktuellen Preisen wieder zu eröffnen, sodass wir ihre Größe wiederherstellen müssen:

//+------------------------------------------------------------------+
//| Check if the daily multiplier needs to be restored               |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::CheckDailyRestore() {
// If the current virtual profit is less than the one desired for recovery,
   if(m_virtualProfit <= RestoreVirtualProfit()) {
      // Restore the daily loss multiplier
      m_dailyDepoPart = 1.0;
      return true;
   }

   return false;
}

//+------------------------------------------------------------------+
//| Check if the overall multiplier needs to be restored             |
//+------------------------------------------------------------------+
bool CVirtualRiskManager::CheckOverallRestore() {
// If the current virtual profit is less than the one desired for recovery,
   if(m_virtualProfit <= RestoreVirtualProfit()) {
      // Restore the overall loss multiplier
      m_overallDepoPart = 1.0;
      return true;
   }

   return false;
}

Die Methode RestoreVirtualProfit() berechnet die gewünschte Höhe des virtuellen Gewinns. Wir haben eine einfache lineare Interpolation mit zwei Parametern verwendet: Je mehr Zeit verstreicht, desto unrentabler wird das Niveau, bei dem wir zustimmen, reale Positionen wieder zu öffnen.

//+------------------------------------------------------------------+
//| Determine the profit of virtual positions for recovery           |
//+------------------------------------------------------------------+
double CVirtualRiskManager::RestoreVirtualProfit() {
// If the maximum recovery time is not specified,
   if(m_maxRestoreTime == 0) {
      // Return the current value of the virtual profit
      return m_virtualProfit;
   }

// Find the elapsed time since the start of recovery in minutes
   double t = (TimeCurrent() - m_startRestoreTime) / 60.0;

// Return the calculated value of the desired virtual profit
// depending on the time elapsed since the start of recovery
   return m_lastVirtualProfit * m_lastVirtualProfitFactor * (1 - t / m_maxRestoreTime);
}

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


Test

Starten wir zunächst den EA mit Parametern, die die vollständige Schließung von Positionen bei Erreichen der Tages- und Gesamtlimits festlegen. Die Ergebnisse sollten die gleichen sein wie in Abb. 3.

Abb. 6. EA-Ergebnisse mit scale_ = 1.0 mit denselben Einstellungen wie der ursprüngliche Risikomanager

Die Ergebnisse für den Drawdown stimmen vollständig überein, während das Ergebnis für den Gewinn und den normalisierten durchschnittlichen Jahresgewinn (OnTester-Ergebnis) leicht über den Erwartungen liegt. Nun, das ist ermutigend: Wir haben keine zuvor implementierten Dinge ‚kaputt‘ gemacht.

Schauen wir uns nun an, was passiert, wenn wir das adaptive Schließen von Positionen bei Erreichen eines Teils der Tages- und Gesamtlimits mit dem Wert 0,5 festlegen:

Abb. 7. EA-Ergebnisse mit scale_ = 1.0, rmCloseDailyPart_ = 0.5 und rmCloseOverallPart_ = 0.5

Der Drawdown verringerte sich leicht, was zu einem leichten Anstieg des normalisierten durchschnittlichen Jahresgewinns führte. Lassen Sie uns nun versuchen, die Wiederherstellung der Positionsgrößen zum besten Preis zu verbinden. Wir setzen die Wartezeit für den besten Einstieg während des Drawdowns in den Parametern auf 1440 Minuten (1 Tag) und den anfänglichen Multiplikator für den besten Drawdown auf 1,5. Es handelt sich dabei nur um intuitiv ermittelte Werte.

Abb. 8. EA Ergebnisse mit scale_ = 1.0, rmCloseDailyPart_ = 0.5, rmCloseOverallPart_ = 0.5, rmMaxRestoreTime_ = 1440 und rmLastVirtualProfitFactor_ = 1.5

Das Ergebnis des normalisierten, durchschnittlichen Jahresgewinns verbesserte sich erneut um einige Prozentpunkte. Es scheint, dass die Bemühungen zur Umsetzung dieses Mechanismus gerechtfertigt sind.

Lassen Sie uns nun versuchen, die Größe der eröffneten Positionen dreimal zu erhöhen, ohne die Parameter des Risikomanagers seit dem letzten Start zu ändern. Dies sollte dazu führen, dass der Risikomanager häufiger ausgelöst wird. Mal sehen, wie er die Aufgabe meistert.

Abb. 9. EA Ergebnisse mit scale_ = 3.0, rmCloseDailyPart_ = 0.5, rmCloseOverallPart_ = 0.5, rmMaxRestoreTime_ = 1440 und rmLastVirtualProfitFactor_ = 1.5

Im Vergleich zu einem ähnlichen Start mit dem ursprünglichen Risikomanager sehen wir auch hier eine Verbesserung der Ergebnisse bei allen Indikatoren. Im Vergleich zum vorherigen Start stieg der Gewinn um etwa das Zweifache, aber der Drawdown stieg um das Dreifache, was den Wert des normalisierten durchschnittlichen Jahresgewinns reduzierte. Für jeden Handelstag überstieg der Drawdown jedoch nicht den in den Parametern des Risikomanagers festgelegten Wert.

Es ist auch interessant zu sehen, was passiert, wenn die Größe der eröffneten Positionen noch weiter erhöht wird, z.B. um das 10-fache?

Abb. 10. EA Ergebnisse mit scale_ = 3.0, rmCloseDailyPart_ = 0.5, rmCloseOverallPart_ = 0.5, rmMaxRestoreTime_ = 1440 und rmLastVirtualProfitFactor_ = 1.5

Wie wir sehen, war der Risikomanager bei solchen Positionsgrößen nicht mehr in der Lage, den vorgegebenen maximalen Gesamtverlust einzuhalten, und der Handel wurde eingestellt. Das bedeutet, dass der Risikomanager kein universelles Instrument ist, mit dem Sie unabhängig von den Eigenheiten einer Handelsstrategie einen bestimmten maximalen Drawdown verhindern können. Sie ergänzt lediglich die obligatorischen Geldmanagementregeln, die in einer Handelsstrategie enthalten sein sollten.

Schließlich wollen wir versuchen, die besten Parameter für den Risikomanager mit Hilfe der genetischen Optimierung auszuwählen. Leider dauert jeder Durchlauf etwa drei Minuten für ein Zweijahresintervall, sodass wir eine beträchtliche Zeit auf den Abschluss der Optimierung warten müssen. Als Optimierungskriterium wählen wir ein Nutzerkriterium, das den normierten durchschnittlichen Jahresgewinn berechnet. Nach einiger Zeit sieht der obere Teil der Tabelle mit den Optimierungsergebnissen wie folgt aus:

Fig. 11. EA-Optimierungsergebnisse mit dem Risikomanager

Wie wir sehen, kann die Auswahl guter Risikomanager-Parameter den Handel nicht nur vor erhöhten Drawdowns schützen, sondern auch die Handelsergebnisse verbessern: Der Unterschied zwischen den verschiedenen Durchgängen betrug 20 % des zusätzlichen Gewinns. Obwohl selbst der beste der bereits gefundenen Parametersätze den in Abb. 8 mit intuitiv gewählten Werten erzielten Ergebnissen leicht unterlegen ist. Eine weitere Optimierung könnte dieses Ergebnis wahrscheinlich noch ein wenig verbessern, aber es besteht kein besonderer Bedarf dafür.


Schlussfolgerung

Fassen wir das Ganze kurz zusammen. Wir haben eine wichtige Komponente jedes erfolgreichen Handelssystems - den Risikomanager - verbessert, indem wir ihn flexibler gemacht haben und an die jeweiligen Bedürfnisse anpassen können. Wir verfügen nun über einen Mechanismus, der es uns ermöglicht, die festgelegten Grenzen für den zulässigen Drawdown genauer einzuhalten.

Obwohl wir versucht haben, ihn so zuverlässig wie möglich zu machen, sollten seine Möglichkeiten mit Bedacht eingesetzt werden. Es ist zu bedenken, dass es sich hierbei um ein extremes Mittel zum Schutz des Kapitals handelt, sodass Sie versuchen müssen, die Handelsparameter so zu wählen, dass der Risikomanager entweder gar nicht oder nur in sehr seltenen Fällen in den Handel eingreifen muss. Wie wir aus den Tests ersehen können, kann die Rate der Aktienveränderung bei einer starken Überschreitung der empfohlenen Positionsgrößen so stark sein, dass selbst der Risikomanager keine Zeit hat, die Mittel vor dem Überschreiten des festgelegten Drawdown-Niveaus zu retten.

Während der Arbeit an dieser Aktualisierung des Risikomanagers kamen neue Ideen zur Verbesserung auf. Allerdings ist es auch nicht gut, wenn man es zu kompliziert macht. Daher werde ich meine Arbeit am Risikomanager für eine Weile beiseite legen und mich in den folgenden Abschnitten dringenderen Fragen der EA-Entwicklung zuwenden.

Vielen Dank für Ihre Aufmerksamkeit! Bis bald!

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

Beigefügte Dateien |
MQL5 beherrschen, vom Anfänger zum Profi (Teil III): Komplexe Datentypen und Include-Dateien MQL5 beherrschen, vom Anfänger zum Profi (Teil III): Komplexe Datentypen und Include-Dateien
Dies ist der dritte Artikel in einer Serie, in der die wichtigsten Aspekte der MQL5-Programmierung beschrieben werden. Dieser Artikel behandelt komplexe Datentypen, die im vorherigen Artikel nicht behandelt wurden. Dazu gehören Strukturen, Unions, Klassen und der Datentyp „function“. Außerdem wird erklärt, wie Sie Ihr Programm mit Hilfe der Präprozessoranweisung #include modularisieren können.
Neuronales Netz in der Praxis: Pseudoinverse (II) Neuronales Netz in der Praxis: Pseudoinverse (II)
Da es sich bei diesen Artikeln um Lehrmaterial handelt und sie nicht dazu gedacht sind, die Implementierung bestimmter Funktionen zu zeigen, werden wir in diesem Artikel ein wenig anders vorgehen. Anstatt zu zeigen, wie man die Faktorisierung anwendet, um die Inverse einer Matrix zu erhalten, werden wir uns auf die Faktorisierung der Pseudoinverse konzentrieren. Der Grund dafür ist, dass es keinen Sinn macht, zu zeigen, wie man den allgemeinen Koeffizienten erhält, wenn man es auf eine spezielle Weise tun kann. Noch besser: Der Leser kann ein tieferes Verständnis dafür entwickeln, warum die Dinge so geschehen, wie sie geschehen. Lassen Sie uns nun herausfinden, warum die Hardware die Software im Laufe der Zeit ersetzt.
Neuronale Netze leicht gemacht (Teil 97): Modelle mit MSFformer trainieren Neuronale Netze leicht gemacht (Teil 97): Modelle mit MSFformer trainieren
Bei der Erforschung verschiedener Modellarchitekturen wird dem Prozess des Modelltrainings oft nicht genügend Aufmerksamkeit geschenkt. In diesem Artikel möchte ich diese Lücke schließen.
Forex-Spread-Handel mit Saisonalität Forex-Spread-Handel mit Saisonalität
Der Artikel untersucht die Möglichkeiten der Erstellung und Bereitstellung von Berichtsdaten über die Verwendung des Saisonalitätsfaktors beim Handel mit Spreads auf dem Forex.