
Entwicklung eines Expertenberaters für mehrere Währungen (Teil 12): Entwicklung eines Risikomanagers auf der Ebene des Eigenhandels
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





- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.