Risikomanager für den manuellen Handel
Inhalt
- Einführung
- Definition der Funktionsweise
- Eingabeparameter und Klassenkonstruktor
- Arbeiten mit Risikolimitzeiten
- Kontrolle der Verwendung von Grenzwerten
- Die Klasse für die Ereignisbehandlung
- Mechanismus zur Kontrolle des täglichen Gewinnziels
- Festlegung einer Methode zur Einleitung der Überwachung in der EA-Struktur
- Die endgültige Implementierung und die Möglichkeiten zur Erweiterung der Klasse
- Beispiel für die Verwendung
- Schlussfolgerung
Einführung
Hallo zusammen! In diesem Artikel werden wir weiter über die Methodik des Risikomanagements sprechen. Im vorangegangenen Artikel „Risikobalance beim gleichzeitigen Handel von mehreren Handelsinstrumenten“ haben wir über die grundlegenden Konzepte des Risikos gesprochen. Jetzt werden wir die grundlegende Risikomanager-Klasse für den sicheren Handel von Grund auf implementieren. Wir werden auch sehen, wie sich die Begrenzung von Risiken in Handelssystemen auf die Wirksamkeit von Handelsstrategien auswirkt.
Der Risk Manager war mein erster Klasse, die ich 2019 geschrieben habe, kurz nachdem ich die Grundlagen des Programmierens erlernt hatte. Damals verstand ich aus eigener Erfahrung, dass die psychologische Verfassung eines Händlers einen großen Einfluss auf die Effektivität des Handels hat, insbesondere wenn es um die „Beständigkeit“ und „Unparteilichkeit“ der Handelsentscheidungen geht. Glücksspiel, emotionale Transaktionen und überhöhte Risiken in dem Versuch, die Verluste so schnell wie möglich auszugleichen, können jedes Konto aufzehren, selbst wenn Sie eine wirksame Handelsstrategie anwenden, die in Tests sehr gute Ergebnisse gezeigt hat.
In diesem Artikel soll gezeigt werden, dass die Risikokontrolle mit Hilfe eines Risikomanagers ihre Wirksamkeit und Zuverlässigkeit erhöht. Um diese These zu bestätigen, werden wir eine einfache Basis-Risikomanagerklasse für den manuellen Handel von Grund auf neu erstellen und sie anhand einer sehr einfachen Fractal-Breakout-Strategie testen.
Definition der Funktionsweise
Wenn wir unseren Algorithmus speziell für den manuellen Handel implementieren, werden wir nur zeitliche Risikolimits für den Tag, die Woche und den Monat kontrollieren. Sobald der tatsächliche Verlustbetrag die vom Nutzer festgelegten Grenzen erreicht oder überschreitet, muss der EA automatisch alle offenen Positionen schließen und den Nutzer über die Unmöglichkeit des weiteren Handels informieren. Dabei ist zu beachten, dass die Informationen nur „beratenden Charakter“ haben, sie werden in der Kommentarzeile in der linken unteren Ecke des Diagramms mit dem laufenden EA angezeigt. Der Grund dafür ist, dass wir einen Risikomanager speziell für den manuellen Handel erstellen, sodass der Nutzer diesen EA jederzeit aus dem Chart entfernen und den Handel fortsetzen kann, wenn es absolut notwendig ist. Ich würde dies jedoch nicht empfehlen, denn wenn der Markt sich gegen Sie wendet, ist es besser, am nächsten Tag zum Handel zurückzukehren und große Verluste zu vermeiden, als zu versuchen, herauszufinden, was genau bei Ihrem manuellen Handel schief gelaufen ist. Wenn Sie diese Klasse in Ihren algorithmischen Handel integrieren, müssen Sie die Beschränkung für das Senden von Aufträgen bei Erreichen des Limits implementieren und diese Klasse vorzugsweise direkt in die EA-Struktur integrieren. Wir werden das später noch genauer erläutern.
Eingabeparameter und Klassenkonstruktor
Wir haben beschlossen, dass wir die Risikokontrolle nur nach Zeiträumen und dem Kriterium der Erreichung der täglichen Gewinnrate durchführen werden. Zu diesem Zweck führen wir mehrere Variablen vom Typ double als Eingabevariable des Speicherklassenmodifikator, die der Nutzer manuell Risikowerte als Prozentsatz der Einlage für jeden Zeitraum sowie den angestrebten täglichen Gewinnprozentsatz zur Gewinnsicherung eingeben kann. Um die Kontrolle des täglichen Gewinnziels anzuzeigen, führen wir eine zusätzliche Variable vom Typ bool ein, mit der diese Funktion aktiviert/deaktiviert werden kann, wenn der Händler jeden Eintrag separat betrachten möchte und sicher ist, dass es keine Korrelation zwischen den ausgewählten Instrumenten gibt. Diese Art von Schaltvariable wird auch als „Flag“ bezeichnet. Lassen Sie uns den folgenden Code auf globaler Ebene deklarieren. Der Einfachheit halber haben wir ihn zuvor mit dem Schlüsselwort group in einen benannten Block „verpackt“.
input group "RiskManagerBaseClass" input double inp_riskperday = 1; // risk per day as a percentage of deposit input double inp_riskperweek = 3; // risk per week input double inp_riskpermonth = 9; // risk per month input double inp_plandayprofit = 3; // target daily profit input bool dayProfitControl = true; // whether to close positions after reaching daily profit
Die deklarierten Variablen werden nach der folgenden Logik mit Standardwerten initialisiert. Wir beginnen mit dem täglichen Risiko, da diese Klasse am besten für den Intraday-Handel geeignet ist, aber auch für den mittelfristigen Handel und Investitionen verwendet werden kann. Wenn Sie mittelfristig oder als Anleger handeln, macht es natürlich keinen Sinn, das Intraday-Risiko zu kontrollieren, und Sie können die gleichen Werte für das tägliche und wöchentliche Risiko festlegen. Wenn Sie nur langfristige Anlagen tätigen, können Sie außerdem alle Grenzwerte auf einen monatlichen Drawdown festlegen. Im Folgenden werden wir uns die Logik der Standardparameter für den Intraday-Handel ansehen.
Wir beschlossen, dass wir mit einem täglichen Risiko von 1 % der Einlage auskommen würden. Wenn das Tageslimit überschritten wird, schließen wir das Terminal bis morgen. Als Nächstes definieren wir die wöchentliche Grenze wie folgt. In der Regel gibt es 5 Handelstage in der Woche, d.h. wenn wir 3 Verlusttage in Folge haben, stellen wir den Handel bis zum Beginn der nächsten Woche ein. Ganz einfach, weil es wahrscheinlicher ist, dass Sie den Markt in dieser Woche nicht verstanden haben oder sich etwas geändert hat, und wenn Sie den Handel fortsetzen, werden Sie in diesem Zeitraum einen so großen Verlust anhäufen, dass Sie nicht einmal in der Lage sein werden, ihn auf Kosten der nächsten Woche zu decken. Eine ähnliche Logik gilt für die Festlegung eines monatlichen Limits beim Intraday-Handel. Wir akzeptieren die Bedingung, dass es bei drei unrentablen Wochen in einem Monat besser ist, die vierte nicht zu handeln, da es viel Zeit in Anspruch nehmen wird, die Renditekurve auf Kosten künftiger Perioden zu „verbessern“. Wir wollen die Anleger auch nicht mit einem großen Verlust in einem einzelnen Monat „erschrecken“.
Wir legen die Höhe des angestrebten Tagesgewinns auf der Grundlage des täglichen Risikos fest und berücksichtigen dabei die Eigenschaften Ihres Handelssystems. Was dabei zu beachten ist. Erstens, ob Sie mit korrelierten Instrumenten handeln, wie oft Ihr Handelssystem Einstiegssignale gibt, ob Sie mit festen Proportionen zwischen Stop-Loss und Take-Profit für jede einzelne Transaktion handeln, oder die Höhe der Einlage. Ich möchte anmerken, dass ich den Handel ohne Stop-Loss und gleichzeitig ohne Risikomanager NICHT EMPFEHLEN kann. Der Verlust Ihrer Einlage ist in diesem Fall nur eine Frage der Zeit. Daher setzen wir entweder für jeden Handel gesonderte Stopps, oder wir verwenden einen Risikomanager, um das Risiko nach Zeiträumen zu begrenzen. In unserem aktuellen Beispiel mit Standardparametern habe ich die Bedingungen für den täglichen Gewinn auf 1 bis 3 im Verhältnis zum täglichen Risiko festgelegt. Es ist auch besser, diese Parameter zusammen mit der obligatorischen Einstellung der Risiko-Ertrags-Relation für JEDEN Handel durch das Verhältnis von Stop-Loss und Take-Profit zu verwenden, ebenfalls 1 bis 3 (Take-Profit ist größer als Stop-Loss).
Die Struktur unserer Grenzwerte kann wie folgt dargestellt werden.
Abbildung 1. Struktur der Grenzwerte
Als Nächstes deklarieren wir unseren nutzerdefinierten Datentyp RiskManagerBase mit dem Schlüsselwort class. Die Eingabeparameter müssen in unserer nutzerdefinierten Klasse RiskManagerBase gespeichert werden. Da unsere Eingabeparameter in Prozenten gemessen werden, während die Limits in der Einzahlungswährung verfolgt werden, müssen wir mehrere entsprechende Felder vom Typ double mit dem Zugriffsmodifikator protected (geschützt) in unsere nutzerdefinierte Klasse eingeben.
protected: double riskperday, // risk per day as a percentage of deposit riskperweek, // risk per week as a percentage of deposit riskpermonth, // risk per month as a percentage of deposit plandayprofit // target daily profit as a percentage of deposit ; double RiskPerDay, // risk per day in currency RiskPerWeek, // risk per week in currency RiskPerMonth, // risk per month in currency StartBalance, // account balance at the EA start time, in currency StartEquity, // account equity at the limit update time, in currency PlanDayEquity, // target account equity value per day, in currency PlanDayProfit // target daily profit, in currency ; double CurrentEquity, // current equity value CurrentBallance; // current balance
Um die Berechnung der Risikolimits nach Zeitraum in der Einzahlungswährung auf der Grundlage der Eingabeparameter zu vereinfachen, werden wir die Methode RefreshLimits() innerhalb unserer Klasse deklarieren, ebenfalls mit dem Zugriffsmodifikator protected. Beschreiben wir diese Methode außerhalb der Klasse wie folgt. Wir werden für die Zukunft einen Rückgabewert vom Typ bool vorsehen, für den Fall, dass wir unsere Methode um die Möglichkeit erweitern müssen, die Korrektheit der erhaltenen Daten zu überprüfen. Wir beschreiben die Methode zunächst in der folgenden Form.
//+------------------------------------------------------------------+ //| RefreshLimits | //+------------------------------------------------------------------+ bool RiskManagerBase::RefreshLimits(void) { CurrentEquity = NormalizeDouble(AccountInfoDouble(ACCOUNT_EQUITY),2); // request current equity value CurrentBallance = NormalizeDouble(AccountInfoDouble(ACCOUNT_BALANCE),2); // request current balance StartBalance = NormalizeDouble(AccountInfoDouble(ACCOUNT_BALANCE),2); // set start balance StartEquity = NormalizeDouble(AccountInfoDouble(ACCOUNT_EQUITY),2); // request current equity value PlanDayProfit = NormalizeDouble(StartEquity * plandayprofit/100,2); // target daily profit, in currency PlanDayEquity = NormalizeDouble(StartEquity + PlanDayProfit/100,2); // target equity, in currency RiskPerDay = NormalizeDouble(StartEquity * riskperday/100,2); // risk per day in currency RiskPerWeek = NormalizeDouble(StartEquity * riskperweek/100,2); // risk per week in currency RiskPerMonth = NormalizeDouble(StartEquity * riskpermonth/100,2); // risk per month in currency return(true); }
Eine bequeme Möglichkeit besteht darin, diese Methode jedes Mal im Code aufzurufen, wenn Grenzwerte bei der Änderung von Zeiträumen neu berechnet werden müssen, sowie bei der ersten Änderung von Feldwerten beim Aufruf des Klassenkonstruktors. Wir schreiben den folgenden Code in den Klassenkonstruktor, um die Startwerte der Felder zu initialisieren.
//+------------------------------------------------------------------+ //| RiskManagerBase | //+------------------------------------------------------------------+ RiskManagerBase::RiskManagerBase() { riskperday = inp_riskperday; // set the value for the internal variable riskperweek = inp_riskperweek; // set the value for the internal variable riskpermonth = inp_riskpermonth; // set the value for the internal variable plandayprofit = inp_plandayprofit; // set the value for the internal variable RefreshLimits(); // update limits }
Nachdem wir die Logik der Eingangsparameter und den Ausgangszustand der Daten für unsere Klasse festgelegt haben, gehen wir zur Implementierung der Abrechnung von Grenzwerten über.
Arbeiten mit Risikolimitzeiten
Um mit Risikobegrenzungszeiträumen zu arbeiten, benötigen wir eine zusätzliche Variable mit geschütztem (protected) Zugriff. Zunächst deklarieren wir unsere eigenen Flags für jede Periode in Form von Variablen des Typs bool, die Daten über das Erreichen der festgelegten Risikolimits speichern, sowie die Haupt-Flag, die über die Möglichkeit informiert, den Handel nur dann fortzusetzen, wenn alle Limits zur gleichen Zeit verfügbar sind. Dies ist notwendig, um zu vermeiden, dass das Monatslimit bereits überschritten ist, aber noch ein Tageslimit besteht und somit der Handel erlaubt ist. Dadurch wird der Handel begrenzt, wenn ein Zeitlimit vor der nächsten Zeitperiode erreicht wird. Wir werden auch Variablen desselben Typs benötigen, um den Tagesgewinn und den Beginn eines neuen Handelstages zu kontrollieren. Außerdem werden wir Felder vom Typ double hinzufügen, um Informationen über den tatsächlichen Gewinn und Verlust für jede Periode zu speichern: Tag, Woche und Monat. Darüber hinaus werden wir separate Werte für Swaps und Provisionen in Handelsgeschäften angeben.
bool RiskTradePermission; // general variable - whether opening of new trades is allowed bool RiskDayPermission; // flag prohibiting trading if daily limit is reached bool RiskWeekPermission; // flag to prohibit trading if daily limit is reached bool RiskMonthPermission; // flag to prohibit trading if monthly limit is reached bool DayProfitArrive; // variable to control if daily target profit is achieved bool NewTradeDay; // variable for a new trading day //--- actual limits double DayorderLoss; // accumulated daily loss double DayorderProfit; // accumulated daily profit double WeekorderLoss; // accumulated weekly loss double WeekorderProfit; // accumulated weekly profit double MonthorderLoss; // accumulated monthly loss double MonthorderProfit; // accumulated monthly profit double MonthOrderSwap; // monthly swap double MonthOrderCommis; // monthly commission
Wir beziehen die Aufwendungen für Provisionen und Swaps ausdrücklich nicht in die Verluste der entsprechenden Zeiträume ein, damit wir in Zukunft die Verluste aus dem Entscheidungsfindungsinstrument von den Verlusten im Zusammenhang mit den Provisions- und Swap-Anforderungen der verschiedenen Makler trennen können. Nachdem wir nun die entsprechenden Felder unserer Klasse deklariert haben, können wir nun die Verwendung von Grenzwerten steuern.
Kontrolle der Verwendung von Grenzwerten
Um die tatsächliche Verwendung von Limits zu kontrollieren, müssen wir Ereignisse im Zusammenhang mit dem Beginn jeder neuen Periode sowie Ereignisse im Zusammenhang mit dem Auftreten abgeschlossener Handelsoperationen behandeln. Um die tatsächlich genutzten Limits korrekt zu verfolgen, werden wir die interne Methode ForOnTrade() im geschützten Zugriffsbereich unserer Klasse bekannt geben.
Zunächst müssen wir in der Methode Variablen für die aktuelle Zeit sowie für die Anfangszeit des Tages, der Woche und des Monats bereitstellen. Für diese Zwecke wird ein spezieller vordefinierter Datentyp vom Typ struct im Format MqlDateTime verwendet. Wir werden sie sofort mit der aktuellen Terminalzeit in der folgenden Form initialisieren.
MqlDateTime local, start_day, start_week, start_month; // create structure to filter dates TimeLocal(local); // fill in initially TimeLocal(start_day); // fill in initially TimeLocal(start_week); // fill in initially TimeLocal(start_month); // fill in initially
Beachten Sie, dass wir für die Initialisierung der aktuellen Zeit die vordefinierte Funktion TimeLocal() anstelle von TimeCurrent() verwenden, da die erste Funktion die lokale Zeit verwendet und die zweite Funktion die Zeit des letzten vom Broker empfangenen Ticks heranzieht, was aufgrund der unterschiedlichen Zeitzonen der verschiedenen Broker zu einer falschen Abrechnung der Limits führen kann. Als Nächstes müssen wir die Startzeit jedes Zeitraums zurücksetzen, um die Werte des Startdatums für jeden einzelnen Zeitraum zu erhalten. Wir werden dies tun, indem wir auf die öffentlichen Felder unserer Strukturen wie folgt zugreifen.
//--- reset to have the report from the beginning of the period start_day.sec = 0; // from the day beginning start_day.min = 0; // from the day beginning start_day.hour = 0; // from the day beginning start_week.sec = 0; // from the week beginning start_week.min = 0; // from the week beginning start_week.hour = 0; // from the week beginning start_month.sec = 0; // from the month beginning start_month.min = 0; // from the month beginning start_month.hour = 0; // from the month beginning
Um die Daten für die Woche und den Monat korrekt zu erhalten, müssen wir die Logik zur Ermittlung des Wochen- und Monatsanfangs definieren. Im Falle eines Monats ist alles ganz einfach, denn wir wissen, dass jeder Monat am ersten Tag beginnt. Der Umgang mit einer Woche ist etwas komplizierter, da es keinen bestimmten Berichtszeitpunkt gibt und sich das Datum jedes Mal ändert. Hier können wir das spezielle day_of_week-Feld der Struktur MqlDateTime verwenden. Mit dieser Funktion können Sie die Nummer des Wochentags vom aktuellen Datum ausgehend bei Null abrufen. Wenn wir diesen Wert kennen, können wir das Anfangsdatum der aktuellen Woche leicht wie folgt herausfinden.
//--- determining the beginning of the week int dif; // day of week difference variable if(start_week.day_of_week==0) // if this is the first day of the week { dif = 0; // then reset } else { dif = start_week.day_of_week-1; // if not the first, then calculate the difference start_week.day -= dif; // subtract the difference at the beginning of the week from the number of the day } //---month start_month.day = 1; // everything is simple with the month
Da wir nun die genauen Anfangsdaten jedes Zeitraums im Verhältnis zum aktuellen Zeitpunkt kennen, können wir nun historische Daten zu den auf dem Konto durchgeführten Transaktionen abfragen. Zunächst müssen wir die notwendigen Variablen deklarieren, um die abgeschlossenen Aufträge zu berücksichtigen, und die Werte der Variablen zurücksetzen, in denen die Finanzergebnisse der Transaktionen für jeden ausgewählten Zeitraum gesammelt werden.
//--- uint total = 0; // number of selected trades ulong ticket = 0; // order number long type; // order type double profit = 0, // order profit commis = 0, // order commission swap = 0; // order swap DayorderLoss = 0; // daily loss without commission DayorderProfit = 0; // daily profit WeekorderLoss = 0; // weekly loss without commission WeekorderProfit = 0; // weekly profit MonthorderLoss = 0; // monthly loss without commission MonthorderProfit = 0; // monthly profit MonthOrderCommis = 0; // monthly commission MonthOrderSwap = 0; // monthly swap
Wir werden historische Daten zu abgeschlossenen Aufträgen über die vordefinierte Terminalfunktion HistorySelect() abfragen. Die Parameter dieser Funktion werden die Daten verwenden, die wir zuvor für jeden Zeitraum erhalten haben. Dazu müssen wir den Typ unserer Variablen MqlDateTime auf den von der Funktion HistorySelect() geforderten Typ bringen, d. h. datetime. Hierfür wird eine vordefinierte Terminalfunktion StructToTime() verwendet. Wir werden die Daten zu den Transaktionen auf die gleiche Weise anfordern, wobei wir die erforderlichen Werte für den Beginn und das Ende des gewünschten Zeitraums ersetzen.
Nach jedem Aufruf der Funktion HistorySelect() müssen wir die Anzahl der ausgewählten Deals mit Hilfe der vordefinierten Terminalfunktion HistoryDealsTotal() ermitteln und diesen Wert in unsere lokale Variable total einsetzen. Nachdem wir die Anzahl der abgeschlossenen Deals ermittelt haben, können wir mit dem for-Operator eine Schleife organisieren, in der wir die Anzahl der einzelnen Deals über die vordefinierte Terminalfunktion HistoryDealGetTicket() abfragen. Dies ermöglicht uns den Zugriff auf die Daten jedes Deals. Der Zugriff auf die Daten einzelner Deals erfolgt über die vordefinierten Terminalfunktionen HistoryDealGetDouble() und HistoryDealGetInteger(), denen die zuvor erhaltene Nummer des Deals übergeben wird. Wir müssen den entsprechenden Bezeichner der Deal-Eigenschaften aus den Enumerationen ENUM_DEAL_PROPERTY_INTEGER und ENUM_DEAL_PROPERTY_DOUBLE angeben. Wir müssen auch einen Filter über einen booleschen Selektionsoperator hinzufügen, wenn nur Handelsgeschäfte aus Handelsoperationen berücksichtigt werden sollen, indem wir nach den Werten DEAL_TYPE_BUY und DEAL_TYPE_SELL aus der Enumeration ENUM_DEAL_TYPE suchen, um andere Kontovorgänge wie Saldo-Transaktionen und Bonusabgrenzungen herauszufiltern. Für die Auswahl der Daten ergibt sich also der folgende Code.
//--- now select data by --==DAY==-- HistorySelect(StructToTime(start_day),StructToTime(local)); // select required history //--- check total = HistoryDealsTotal(); // number number of selected deals ticket = 0; // order number profit = 0; // order profit commis = 0; // order commission swap = 0; // order swap //--- for all deals for(uint i=0; i<total; i++) // loop through all selected orders { //--- try to get deals ticket if((ticket=HistoryDealGetTicket(i))>0) // get the number of each in order { //--- get deals properties profit = HistoryDealGetDouble(ticket,DEAL_PROFIT); // get data on financial results commis = HistoryDealGetDouble(ticket,DEAL_COMMISSION); // get data on commission swap = HistoryDealGetDouble(ticket,DEAL_SWAP); // get swap data type = HistoryDealGetInteger(ticket,DEAL_TYPE); // get data on operation type if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL) // if the deal is form a trading operatoin { if(profit>0) // if financial result of current order is greater than 0, { DayorderProfit += profit; // add to profit } else { DayorderLoss += MathAbs(profit); // if loss, add up } } } } //--- now select data by --==WEEK==-- HistorySelect(StructToTime(start_week),StructToTime(local)); // select the required history //--- check total = HistoryDealsTotal(); // number number of selected deals ticket = 0; // order number profit = 0; // order profit commis = 0; // order commission swap = 0; // order swap //--- for all deals for(uint i=0; i<total; i++) // loop through all selected orders { //--- try to get deals ticket if((ticket=HistoryDealGetTicket(i))>0) // get the number of each in order { //--- get deals properties profit = HistoryDealGetDouble(ticket,DEAL_PROFIT); // get data on financial results commis = HistoryDealGetDouble(ticket,DEAL_COMMISSION); // get data on commission swap = HistoryDealGetDouble(ticket,DEAL_SWAP); // get swap data type = HistoryDealGetInteger(ticket,DEAL_TYPE); // get data on operation type if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL) // if the deal is form a trading operatoin { if(profit>0) // if financial result of current order is greater than 0, { WeekorderProfit += profit; // add to profit } else { WeekorderLoss += MathAbs(profit); // if loss, add up } } } } //--- now select data by --==MONTH==-- HistorySelect(StructToTime(start_month),StructToTime(local)); // select the required history //--- check total = HistoryDealsTotal(); // number number of selected deals ticket = 0; // order number profit = 0; // order profit commis = 0; // order commission swap = 0; // order swap //--- for all deals for(uint i=0; i<total; i++) // loop through all selected orders { //--- try to get deals ticket if((ticket=HistoryDealGetTicket(i))>0) // get the number of each in order { //--- get deals properties profit = HistoryDealGetDouble(ticket,DEAL_PROFIT); // get data on financial results commis = HistoryDealGetDouble(ticket,DEAL_COMMISSION); // get data on commission swap = HistoryDealGetDouble(ticket,DEAL_SWAP); // get swap data type = HistoryDealGetInteger(ticket,DEAL_TYPE); // get data on operation type MonthOrderSwap += swap; // sum up swaps MonthOrderCommis += commis; // sum up commissions if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL) // if the deal is form a trading operatoin { if(profit>0) // if financial result of current order is greater than 0, { MonthorderProfit += profit; // add to profit } else { MonthorderLoss += MathAbs(profit); // if loss, sum up } } } }
Die obige Methode kann jedes Mal aufgerufen werden, wenn wir die aktuellen Grenzwerte für die Verwendung aktualisieren müssen. Wir können die Werte der aktuellen Grenzwerte aktualisieren und diese Funktion aufrufen, wenn wir verschiedene Terminalereignisse erzeugen. Da der Zweck dieser Methode darin besteht, die Limits zu aktualisieren, kann dies geschehen, wenn Ereignisse im Zusammenhang mit Änderungen bei laufenden Aufträgen auftreten, wie z. B. Trade und TradeTransaction, und wenn ein neuer Tick mit dem Ereignis NewTick auftritt. Da unsere Methode recht ressourceneffizient ist, werden wir die aktuellen Grenzwerte bei jedem Tick aktualisieren. Implementieren wir nun die Ereignisbehandlungsroutine, die für die Behandlung von Ereignissen im Zusammenhang mit der dynamischen Stornierung und der Handelsauflösung erforderlich ist.
Die Klasse für die Ereignisbehandlung
Zur Behandlung von Ereignissen definieren wir eine interne Methode unserer Klasse ContoEvents() mit der Zugriffsebene protected. Zu diesem Zweck deklarieren wir zusätzliche Hilfsfelder mit der gleichen Zugriffsebene. Um den Startzeitpunkt einer neuen Handelsperiode, den wir für die Änderung der Handelserlaubnisflags benötigen, sofort nachvollziehen zu können, müssen wir die Werte der letzten aufgezeichneten Periode und der aktuellen Periode speichern. Für diese Zwecke können wir einfache Arrays verwenden, die mit dem Datentyp datetime deklariert sind, um die Werte der entsprechenden Zeiträume zu speichern.
//--- additional auxiliary arrays datetime Periods_old[3]; // 0-day,1-week,2-mn datetime Periods_new[3]; // 0-day,1-week,2-mn
In der ersten Dimension werden wir die Werte des Tages, in der zweiten die der Woche und in der dritten die des Monats speichern. Wenn es notwendig ist, die kontrollierten Zeiträume weiter auszudehnen, können Sie diese Arrays nicht statisch, sondern dynamisch deklarieren. Wir arbeiten hier aber nur mit drei Zeiträumen. Fügen wir nun in unserem Klassenkonstruktor die primäre Initialisierung dieser Array-Variablen wie folgt hinzu.
Periods_new[0] = iTime(_Symbol, PERIOD_D1, 1); // initialize the current day with the previous period Periods_new[1] = iTime(_Symbol, PERIOD_W1, 1); // initialize the current week with the previous period Periods_new[2] = iTime(_Symbol, PERIOD_MN1, 1); // initialize the current month with the previous period
Wir werden jede entsprechende Periode mit einer vordefinierten Terminalfunktion iTime() initialisieren, die in den Parametern die entsprechende Zeitrahmen von ENUM_TIMEFRAMES aus dem Zeitrahmen vor dem aktuellen Zeitrahmen übergibt. Das Array Periods_old[] wird absichtlich nicht initialisiert. In diesem Fall stellen wir nach dem Aufruf des Konstruktors und unserer ContoEvents()-Methode sicher, dass das Ereignis des Beginns der neuen Handelsperiode ausgelöst wird und alle Flags für den Handelsbeginn geöffnet und erst dann durch den Code geschlossen werden, wenn es keine Limits mehr gibt. Andernfalls funktioniert die Klasse bei der Neuinitialisierung möglicherweise nicht richtig. Die beschriebene Methode enthält eine einfache Logik: Wenn der aktuelle Zeitraum nicht gleich dem vorherigen ist, bedeutet dies, dass ein neuer entsprechender Zeitraum begonnen hat, und Sie können die Limits zurücksetzen und den Handel zulassen, indem Sie die Werte in den Flags ändern. Außerdem wird für jeden Zeitraum die zuvor beschriebene Methode RefreshLimits() aufgerufen, um die Eingabegrenzen neu zu berechnen.
//+------------------------------------------------------------------+ //| ContoEvents | //+------------------------------------------------------------------+ void RiskManagerBase::ContoEvents() { // check the start of a new trading day NewTradeDay = false; // variable for new trading day set to false Periods_old[0] = Periods_new[0]; // copy to old, new Periods_new[0] = iTime(_Symbol, PERIOD_D1, 0); // update new for day if(Periods_new[0]!=Periods_old[0]) // if do not match, it's a new day { Print(__FUNCTION__+" line"+IntegerToString(__LINE__)+", New trade day!"); // inform NewTradeDay = true; // variable to true DayProfitArrive = false; // reset flag of reaching target profit after a new day started RiskDayPermission = true; // allow opening new positions RefreshLimits(); // update limits DayorderLoss = 0; // reset daily financial result DayorderProfit = 0; // reset daily financial result } // check the start of a new trading week Periods_old[1] = Periods_new[1]; // copy data to old period Periods_new[1] = iTime(_Symbol, PERIOD_W1, 0); // fill new period for week if(Periods_new[1]!= Periods_old[1]) // if periods do not match, it's a new week { Print(__FUNCTION__+" line"+IntegerToString(__LINE__)+", New trade week!"); // inform RiskWeekPermission = true; // allow opening new positions RefreshLimits(); // update limits WeekorderLoss = 0; // reset weekly losses WeekorderProfit = 0; // reset weekly profits } // check the start of a new trading month Periods_old[2] = Periods_new[2]; // copy the period to the old one Periods_new[2] = iTime(_Symbol, PERIOD_MN1, 0); // update new period for month if(Periods_new[2]!= Periods_old[2]) // if do not match, it's a new month { Print(__FUNCTION__+" line"+IntegerToString(__LINE__)+", New trade Month!"); // inform RiskMonthPermission = true; // allow opening new positions RefreshLimits(); // update limits MonthorderLoss = 0; // reset the month's loss MonthorderProfit = 0; // reset the month's profit } // set the permission to open new positions true only if everything is true // set to true if(RiskDayPermission == true && // if there is a daily limit available RiskWeekPermission == true && // if there is a weekly limit available RiskMonthPermission == true // if there is a monthly limit available ) // { RiskTradePermission=true; // if all are allowed, trading is allowed } // set to false if at least one of them is false if(RiskDayPermission == false || // no daily limit available RiskWeekPermission == false || // or no weekly limit available RiskMonthPermission == false || // or no monthly limit available DayProfitArrive == true // or target profit is reached ) // then { RiskTradePermission=false; // prohibit trading } }
Auch in dieser Methode haben wir die Kontrolle über den Zustand der Daten in der Hauptvariablen das Flag für die Möglichkeit der Eröffnung neuer Positionen, RiskTradePermission, hinzugefügt. Mit Hilfe von logischen Auswahloperatoren wird die Erlaubnis über diese Variable nur dann erteilt, wenn alle Erlaubnisse wahr sind, und sie wird deaktiviert, wenn mindestens eine der Flags den Handel nicht zulässt. Diese Variable wird sehr nützlich sein, wenn Sie diese Klasse in einen bereits erstellten algorithmischen EA integrieren; Sie können sie einfach über eine Get-Funktion erhalten und in den Code mit den Bedingungen für die Platzierung Ihrer Aufträge einfügen. In unserem Fall wird es einfach als Flag dienen, um den Nutzer über das Fehlen von freien Handelslimits zu informieren. Nachdem unsere Klasse nun „gelernt“ hat, wie man Risiken kontrolliert, wenn die festgelegten Verluste erreicht werden, wollen wir nun die Funktionalität zur Kontrolle des Erreichens des Gewinnziels implementieren.
Mechanismus zur Kontrolle des täglichen Gewinnziels
Im vorangegangenen Teil unserer Artikel haben wir ein Flag für den Start der Kontrolle über den Gewinnziel und eine Eingabevariable für die Bestimmung ihres Wertes in Abhängigkeit von der Höhe der Kontoeinlage deklariert. Nach der Logik unserer Klasse, die das Erreichen des Gewinnziels kontrolliert, werden alle offenen Positionen geschlossen, wenn der Gesamtgewinn für alle Positionen den Zielwert erreicht hat. Um alle Positionen eines Kontos zu schließen, deklarieren wir in unserer Klasse die interne Methode AllOrdersClose() mit der Zugriffsebene public (öffentlich). Damit diese Methode funktioniert, müssen wir Daten über offene Positionen erhalten und automatisch Aufträge senden, um sie zu schließen.
Um keine Zeit damit zu verschwenden, eigene Implementierungen dieser Funktionalität zu schreiben, werden wir fertige interne Klassen des Terminals verwenden. Wir werden die interne Standardterminalklasse CPositionInfo verwenden, um mit offenen Positionen zu arbeiten, und die Klasse CTrade, um offene Positionen zu schließen. Deklarieren wir die Variablen dieser beiden Klassen auch mit der geschützten Zugriffsebene ohne Zeiger mit Standardkonstruktor wie folgt.
CTrade r_trade; // instance CPositionInfo r_position; // instance
Wenn wir mit diesen Objekten arbeiten, brauchen wir sie im Rahmen der jetzt benötigten Funktionalität nicht zusätzlich zu konfigurieren, also schreiben wir sie nicht in den Konstruktor unserer Klasse. Hier ist die Implementierung dieser Methode unter Verwendung deklarierter Klassen:
//+------------------------------------------------------------------+ //| AllOrdersClose | //+------------------------------------------------------------------+ bool RiskManagerBase::AllOrdersClose() // closing market positions { ulong ticket = 0; // order ticket string symb; for(int i = PositionsTotal(); i>=0; i--) // loop through open positoins { if(r_position.SelectByIndex(i)) // if a position selected { ticket = r_position.Ticket(); // remember position ticket if(!r_trade.PositionClose(ticket)) // close by ticket { Print(__FUNCTION__+". Error close order. "+IntegerToString(ticket)); // if not, inform return(false); // return false } else { Print(__FUNCTION__+". Order close success. "+IntegerToString(ticket)); // if not, inform continue; // if everything is ok, continue } } } return(true); // return true }
Wir werden die beschriebene Methode sowohl bei Erreichen des Gewinnziels als auch bei Erreichen der Grenzen aufrufen. Sie gibt auch einen bool-Wert zurück, falls es notwendig ist, um Fehler beim Senden von Abschlussaufträgen zu behandeln. Um zu kontrollieren, ob das Gewinnziel erreicht wird, ergänzen wir unsere Ereignisbehandlungsmethode ContoEvents() um den folgenden Code, der sich unmittelbar an den bereits oben beschriebenen Code anschließt.
//--- daily if(dayProfitControl) // check if functionality is enabled by the user { if(CurrentEquity >= (StartEquity+PlanDayProfit)) // if equity exceeds or equals start + target profit, { DayProfitArrive = true; // set flag that target profit is reached Print(__FUNCTION__+", PlanDayProfit has been arrived."); // inform about the event Print(__FUNCTION__+", CurrentEquity = "+DoubleToString(CurrentEquity)+ ", StartEquity = "+DoubleToString(StartEquity)+ ", PlanDayProfit = "+DoubleToString(PlanDayProfit)); AllOrdersClose(); // close all open orders StartEquity = CurrentEquity; // rewrite starting equity value //--- send a push notification ResetLastError(); // reset the last error if(!SendNotification("The planned profitability for the day has been achieved. Equity: "+DoubleToString(CurrentEquity)))// notification { Print(__FUNCTION__+IntegerToString(__LINE__)+", Error of sending notification: "+IntegerToString(GetLastError()));// if not, print } } }
Das Verfahren umfasst das Senden einer Push-Benachrichtigung an den Nutzer, um ihn über das Eintreten dieses Ereignisses zu informieren. Hierfür verwenden wir die vordefinierte Terminalfunktion SendNotification. Um die erforderliche Mindestfunktionalität unserer Klasse zu vervollständigen, müssen wir nur noch eine weitere Klassenmethode mit öffentlichem (public) Zugriff zusammenstellen, die aufgerufen wird, wenn ein Risikomanager mit der Struktur unseres EA verbunden wird.
Festlegung einer Methode zur Einleitung der Überwachung in der EA-Struktur
Um die Überwachungsfunktionalität von einer Instanz unserer Risikomanager-Klasse zur EA-Struktur hinzuzufügen, deklarieren wir die öffentliche Methode ContoMonitor(). In dieser Methode sammeln wir alle zuvor deklarierten Methoden zur Ereignisbehandlung und ergänzen sie um eine Funktionalität zum Vergleich der tatsächlich verwendeten Grenzwerte mit den vom Nutzer in den Eingabeparametern freigegebenen Werten. Wir deklarieren diese Methode mit der Zugriffsebene public und beschreiben sie außerhalb der Klasse wie folgt.
//+------------------------------------------------------------------+ //| ContoMonitor | //+------------------------------------------------------------------+ void RiskManagerBase::ContoMonitor() // monitoring { ForOnTrade(); // update at each tick ContoEvents(); // event block //--- double currentProfit = AccountInfoDouble(ACCOUNT_PROFIT); if((MathAbs(DayorderLoss)+MathAbs(currentProfit) >= RiskPerDay && // if equity is less than or equal to the start balance minus the daily risk currentProfit<0 && // profit below zero RiskDayPermission==true) // day trading is allowed || // OR (RiskDayPermission==true && // day trading is allowed MathAbs(DayorderLoss) >= RiskPerDay) // loss exceed daily risk ) { Print(__FUNCTION__+", EquityControl, "+"ACCOUNT_PROFIT = " +DoubleToString(currentProfit));// notify Print(__FUNCTION__+", EquityControl, "+"RiskPerDay = " +DoubleToString(RiskPerDay)); // notify Print(__FUNCTION__+", EquityControl, "+"DayorderLoss = " +DoubleToString(DayorderLoss)); // notify RiskDayPermission=false; // prohibit opening new orders during the day AllOrdersClose(); // close all open positions } // check if there is a WEEK limit available for opening a new position if there are no open ones if( MathAbs(WeekorderLoss)>=RiskPerWeek && // if weekly loss is greater than or equal to the weekly risk RiskWeekPermission==true) // and we traded { RiskWeekPermission=false; // prohibit opening of new orders during the day AllOrdersClose(); // close all open positions Print(__FUNCTION__+", EquityControl, "+"WeekorderLoss = "+DoubleToString(WeekorderLoss)); // notify Print(__FUNCTION__+", EquityControl, "+"RiskPerWeek = "+DoubleToString(RiskPerWeek)); // notify } // check if there is a MONTH limit available for opening a new position if there are no open ones if( MathAbs(MonthorderLoss)>=RiskPerMonth && // if monthly loss is greater than or equal to the monthly risk RiskMonthPermission==true) // we traded { RiskMonthPermission=false; // prohibit opening of new orders during the day AllOrdersClose(); // close all open positions Print(__FUNCTION__+", EquityControl, "+"MonthorderLoss = "+DoubleToString(MonthorderLoss)); // notify Print(__FUNCTION__+", EquityControl, "+"RiskPerMonth = "+DoubleToString(RiskPerMonth)); // notify } }
Die Funktionslogik unserer Methode ist sehr einfach: Wenn die tatsächliche Verlustgrenze für einen Monat oder eine Woche die vom Nutzer festgelegte Grenze überschreitet, wird das Handelskennzeichen für einen bestimmten Zeitraum auf „verboten“ gesetzt und der Handel dementsprechend untersagt. Der einzige Unterschied besteht in den täglichen Limits, wo wir auch das Vorhandensein offener Positionen kontrollieren müssen; dazu werden wir auch die Kontrolle des aktuellen Gewinns aus offenen Positionen durch den logischen Operator OR hinzufügen. Wenn die Risikogrenzen erreicht sind, rufen wir unsere Methode zum Schließen von Positionen auf und drucken das Protokoll über dieses Ereignis.
In diesem Stadium, in dem die Klasse vollständig ist, müssen wir nur noch eine Methode hinzufügen, mit der der Nutzer die aktuellen Grenzwerte kontrollieren kann. Der einfachste und bequemste Weg wäre, die notwendigen Informationen über die vordefinierte Standardterminalfunktion Comment() anzuzeigen. Um mit dieser Funktion zu arbeiten, müssen wir ihr einen Parameter vom Typ String übergeben, der Informationen enthält, die im Chart angezeigt werden sollen. Um diese Werte von unserer Klasse zu erhalten, deklarieren wir die Methode Message() mit der Zugriffsebene public, die String-Daten mit gesammelten Daten zu allen Variablen zurückgibt, die der Nutzer benötigt.
//+------------------------------------------------------------------+ //| Message | //+------------------------------------------------------------------+ string RiskManagerBase::Message(void) { string msg; // message msg += "\n"+" ----------Risk-Manager---------- "; // common //--- msg += "\n"+"RiskTradePer = "+(string)RiskTradePermission; // final trade permission msg += "\n"+"RiskDayPer = "+(string)RiskDayPermission; // daily risk available msg += "\n"+"RiskWeekPer = "+(string)RiskWeekPermission; // weekly risk available msg += "\n"+"RiskMonthPer = "+(string)RiskMonthPermission; // monthly risk available //---limits and inputs msg += "\n"+" -------------------------------- "; // msg += "\n"+"RiskPerDay = "+DoubleToString(RiskPerDay,2); // daily risk in usd msg += "\n"+"RiskPerWeek = "+DoubleToString(RiskPerWeek,2); // weekly risk in usd msg += "\n"+"RiskPerMonth = "+DoubleToString(RiskPerMonth,2); // monthly risk usd //--- current profits and losses for periods msg += "\n"+" -------------------------------- "; // msg += "\n"+"DayLoss = "+DoubleToString(DayorderLoss,2); // daily loss msg += "\n"+"DayProfit = "+DoubleToString(DayorderProfit,2); // daily profit msg += "\n"+"WeekLoss = "+DoubleToString(WeekorderLoss,2); // weekly loss msg += "\n"+"WeekProfit = "+DoubleToString(WeekorderProfit,2); // weekly profit msg += "\n"+"MonthLoss = "+DoubleToString(MonthorderLoss,2); // monthly loss msg += "\n"+"MonthProfit = "+DoubleToString(MonthorderProfit,2); // monthly profit msg += "\n"+"MonthCommis = "+DoubleToString(MonthOrderCommis,2); // monthly commissions msg += "\n"+"MonthSwap = "+DoubleToString(MonthOrderSwap,2); // monthly swaps //--- for current monitoring if(dayProfitControl) // if control daily profit { msg += "\n"+" ---------dayProfitControl-------- "; // msg += "\n"+"DayProfitArrive = "+(string)DayProfitArrive; // daily profit achieved msg += "\n"+"StartBallance = "+DoubleToString(StartBalance,2); // starting balance msg += "\n"+"PlanDayProfit = "+DoubleToString(PlanDayProfit,2); // target profit msg += "\n"+"PlanDayEquity = "+DoubleToString(PlanDayEquity,2); // target equity } return(msg); // return value }
Die Nachricht für den durch die Methode erstellten Nutzer sieht wie folgt aus.
Abbildung 2. Format der Datenausgabe.
Diese Methode kann durch das Hinzufügen von Elementen für die Arbeit mit Grafiken im Terminal geändert oder ergänzt werden. Wir werden es aber so verwenden, da es dem Nutzer genügend Daten aus unserer Klasse liefert, um eine Entscheidung zu treffen. Falls gewünscht, können Sie dieses Format in Zukunft verfeinern und grafisch aufwerten. Erörtern wir nun die Möglichkeiten der Erweiterung dieser Klasse bei der Verwendung einzelner Handelsstrategien.
Die endgültige Implementierung und die Möglichkeiten zur Erweiterung der Klasse
Wie wir bereits erwähnt haben, ist die hier beschriebene Funktionalität das notwendige Minimum und die universellste für fast alle Handelsstrategien. Sie ermöglicht es, Risiken zu kontrollieren und den Verlust von Einlagen an einem Tag zu verhindern. In diesem Teil des Artikels werden wir uns einige weitere Möglichkeiten zur Erweiterung dieser Klasse ansehen.
- Kontrolle der Spread-Größe beim Handel mit einem kurzen Stop-Loss
- Kontrolle des Schlupfes (Slippage) für offene Positionen
- Kontrolle des monatlichen Gewinnziels
Für den ersten Punkt können wir zusätzliche Funktionen für Handelssysteme implementieren, die den Handel mit kurzen Stop-Loss verwenden. Sie können die Methode SpreadMonitor(int intSL) deklarieren, die als Parameter den technischen oder berechneten Stop-Loss für ein Instrument in Punkten erhält, um ihn mit dem aktuellen Spread-Level zu vergleichen. Diese Methode verbietet die Platzierung eines Auftrags, wenn sich der Spread im Verhältnis zum Stop-Loss in einem vom Nutzer festgelegten Verhältnis stark ausweitet, um das hohe Risiko zu vermeiden, die Position aufgrund des Spreads beim Stop-Loss zu schließen.
Um den Schlupf zum Zeitpunkt der Öffnung zu kontrollieren, können Sie gemäß dem zweiten Punkt die Methode SlippageCheck() deklarieren. Mit dieser Methode wird jedes einzelne Handelsgeschäft geschlossen, wenn der Makler es zu einem Preis eröffnet hat, der sich stark von dem angegebenen Preis unterscheidet, sodass das Geschäftsrisiko den erwarteten Wert übersteigt. Dies ermöglicht es, bei Auslösung des Stop-Loss die Statistik nicht durch risikoreichen Handel mit einem separaten Eintrag zu verderben. Auch beim Handel mit einem festen Stop-Loss-Take-Profit-Verhältnis verschlechtert sich dieses Verhältnis aufgrund von Slippage, und es ist besser, die Position mit einem kleinen Verlust zu schließen, als später größere Verluste zu erleiden.
Ähnlich der Logik der Steuerung des Tagesgewinns ist es möglich, eine entsprechende Methode zur Steuerung des monatlichen Gewinnziels zu implementieren. Diese Methode kann beim Handel mit längerfristigen Strategien eingesetzt werden. Die von uns beschriebene Klasse verfügt bereits über alle erforderlichen Funktionen für den manuellen Intraday-Handel und kann in die endgültige Implementierung eines Handels-EAs integriert werden, der gleichzeitig mit dem Start des manuellen Handels auf dem Instrumentenchart gestartet werden sollte.
Bei der endgültigen Zusammenstellung des Projekts wird unsere Klasse mit Hilfe der Präprozessoranweisung #include eingebunden.
#include <RiskManagerBase.mqh>
Als Nächstes deklarieren wir den Zeiger unseres Risikomanagerobjekts auf globaler Ebene.
RiskManagerBase *RMB;
Bei der Initialisierung unseres EA weisen wir unserem Objekt manuell Speicher zu, um es vor dem Start vorzubereiten.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { RMB = new RiskManagerBase(); //--- return(INIT_SUCCEEDED); }
Wenn wir unseren EA aus dem Chart entfernen, müssen wir den Speicher aus unserem Objekt löschen, um ein Speicherleck zu vermeiden. Dazu schreiben Sie Folgendes in die Funktion OnDeinit des EA.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- delete RMB; }
Falls erforderlich, können Sie im selben Ereignis auch die Methode Comment(“ „) aufrufen und ihr einen leeren String übergeben, damit das Chart von Kommentaren befreit wird, wenn der EA aus dem Symboldiagramm entfernt wird.
Wir rufen die Hauptüberwachungsmethode unserer Klasse auf, wenn wir einen neuen Tick für das Symbol erhalten.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { RMB.ContoMonitor(); Comment(RMB.Message()); } //+------------------------------------------------------------------+
Damit ist die Zusammenstellung unseres EA mit dem eingebauten Risikomanager abgeschlossen und er ist vollständig einsatzbereit (Datei ManualRiskManager.mq5). Um mehrere Anwendungsfälle zu testen, werden wir eine kleine Ergänzung des aktuellen Codes vornehmen, um den Prozess des manuellen Handels zu simulieren.
Beispiel für die Verwendung
Um den Prozess des manuellen Handels mit und ohne Verwendung eines Risikomanagers zu visualisieren, benötigen wir zusätzlichen Code, der den manuellen Handel modelliert. Da wir in diesem Artikel nicht auf die Auswahl von Handelsstrategien eingehen werden, werden wir auch nicht die gesamte Handelsfunktionalität in Code implementieren. Stattdessen werden wir visuell Einträge aus dem Tageschart nehmen und vorgefertigte Daten in unseren EA einfügen. Wir werden eine sehr einfache Strategie verwenden, um Handelsentscheidungen zu treffen, und wir werden das finanzielle Endergebnis für diese Strategie mit dem einzigen Unterschied sehen: mit und ohne Risikokontrolle.
Als Beispiel für den Einstieg verwenden wir eine einfache Strategie mit Ausbrüchen aus einem fraktalen Niveau für das Instrument USDJPY über einen Zeitraum von zwei Monaten. Schauen wir uns an, wie diese Strategie mit und ohne Risikokontrolle abschneidet. Schematisch sehen die Strategiesignale für manuelle Eingaben wie folgt aus.
Abbildung 3. Eingaben mit einer Teststrategie
Um diese Strategie zu modellieren, schreiben wir einen kleinen Zusatz als universellen Einheitstest für jede manuelle Strategie, sodass jeder Nutzer seine Angaben mit kleinen Änderungen testen kann. Dieser Test wird darin bestehen, bereits vorbereitete Signale für die Umsetzung zu laden, ohne die Logik der einzelnen Eingaben zu kennen. Dazu müssen wir zunächst eine zusätzliche Struktur, struct, deklarieren, die unsere fraktal-basierten Einträge speichern wird.
//+------------------------------------------------------------------+ //| TradeInputs | //+------------------------------------------------------------------+ struct TradeInputs { string symbol; // symbol ENUM_POSITION_TYPE direction; // direction double price; // price datetime tradedate; // date bool done; // trigger flag };
Die Hauptklasse, die für die Modellierung von Handelssignalen verantwortlich sein wird, ist TradeModel. Der Klassenkonstruktor akzeptiert einen Container mit Signaleingangsparametern, und seine Hauptmethode Processing() überwacht bei jedem Tick, ob der Zeitpunkt des Eintrittspunkts auf der Grundlage der Eingangswerte erreicht ist. Da wir den Intraday-Handel simulieren, werden wir am Ende des Tages alle Positionen mithilfe der zuvor deklarierten Methode AllOrdersClose() in unserer Risikomanager-Klasse auflösen. Hier ist unsere Hilfsklasse.
//+------------------------------------------------------------------+ //| TradeModel | //+------------------------------------------------------------------+ class TradeModel { protected: CTrade *cTrade; // to trade TradeInputs container[]; // container of entries int size; // container size public: TradeModel(const TradeInputs &inputs[]); ~TradeModel(void); void Processing(); // main modeling method };
Um eine bequeme Auftragserteilung zu ermöglichen, werden wir die Standardterminalklasse CTrade verwenden, die alle benötigten Funktionen enthält. Das spart Zeit bei der Entwicklung unserer Hilfsklasse. Um beim Anlegen einer Klasseninstanz Eingabeparameter zu übergeben, definieren wir unseren Konstruktor mit einem Eingabeparameter des Eingabecontainers.
//+------------------------------------------------------------------+ //| TradeModel | //+------------------------------------------------------------------+ TradeModel::TradeModel(const TradeInputs &inputs[]) { size = ArraySize(inputs); // get container size ArrayResize(container, size); // resize for(int i=0; i<size; i++) // loop through inputs { container[i] = inputs[i]; // copy to internal } //--- trade class cTrade=new CTrade(); // create trade instance if(CheckPointer(cTrade)==POINTER_INVALID) // if instance not created, { Print(__FUNCTION__+IntegerToString(__LINE__)+" Error creating object!"); // notify } cTrade.SetTypeFillingBySymbol(Symbol()); // fill type for the symbol cTrade.SetDeviationInPoints(1000); // deviation cTrade.SetExpertMagicNumber(123); // magic number cTrade.SetAsyncMode(false); // asynchronous method }
Im Konstruktor initialisieren wir den Container der Eingabeparameter mit dem gewünschten Wert, merken uns seine Größe und erstellen ein Objekt unserer CTrade-Klasse mit den notwendigen Einstellungen. Die meisten Parameter werden hier nicht vom Nutzer konfiguriert, da sie den Zweck der Erstellung unseres Einheitstests nicht beeinträchtigen, sodass wir sie fest kodiert lassen.
Der Destruktor unserer TradeModel-Klasse erfordert nur das Entfernen eines CTrade-Objekts.
//+------------------------------------------------------------------+ //| ~TradeModel | //+------------------------------------------------------------------+ TradeModel::~TradeModel(void) { if(CheckPointer(cTrade)!=POINTER_INVALID) // if there is an instance, { delete cTrade; // delete } }
Nun werden wir unsere Hauptverarbeitungsmethode für den Betrieb unserer Klasse in die Struktur unseres gesamten Projekts implementieren. Lassen Sie uns die Logik für die Auftragserteilung gemäß Abbildung 3 implementieren:
//+------------------------------------------------------------------+ //| Processing | //+------------------------------------------------------------------+ void TradeModel::Processing(void) { datetime timeCurr = TimeCurrent(); // request current time double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); // take bid double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); // take ask for(int i=0; i<size; i++) // loop through inputs { if(container[i].done==false && // if we haven't traded yet AND container[i].tradedate <= timeCurr) // date is correct { switch(container[i].direction) // check trade direction { //--- case POSITION_TYPE_BUY: // if Buy, if(container[i].price >= ask) // check if price has reached and { if(cTrade.Buy(0.1)) // by the same lot { container[i].done = true; // if time has passed, put a flag Print("Buy has been done"); // notify } else // if hasn't passed, { Print("Error: buy"); // notify } } break; // complete the case //--- case POSITION_TYPE_SELL: // if Sell if(container[i].price <= bid) // check if price has reached and { if(cTrade.Sell(0.1)) // sell the same lot { container[i].done = true; // if time has passed, put a flag Print("Sell has been done"); // notify } else // if hasn't passed, { Print("Error: sell"); // notify } } break; // complete the case //--- default: Print("Wrong inputs"); // notify return; break; } } } }
Die Logik dieser Methode ist recht einfach. Wenn sich im Container noch nicht verarbeitete Einträge befinden, für die die Modellierungszeit gekommen ist, platzieren wir diese Aufträge entsprechend der Richtung und dem Preis des in Abbildung 3 markierten Fraktals. Diese Funktionalität reicht aus, um den Risikomanager zu testen, sodass wir ihn in unser Hauptprojekt integrieren können.
Verbinden wir zunächst unsere Testklasse wie folgt mit dem EA-Code.
#include <TradeModel.mqh>
In der Funktion OnInit() erstellen wir nun eine Instanz unserer TradeInputs-Datenarray-Struktur und übergeben dieses Array an den Konstruktor der TradeModel-Klasse, um es zu initialisieren.
//--- TradeInputs modelInputs[] = { {"USDJPYz", POSITION_TYPE_SELL, 146.636, D'2024-01-31',false}, {"USDJPYz", POSITION_TYPE_BUY, 148.794, D'2024-02-05',false}, {"USDJPYz", POSITION_TYPE_BUY, 148.882, D'2024-02-08',false}, {"USDJPYz", POSITION_TYPE_SELL, 149.672, D'2024-02-08',false} }; //--- tModel = new TradeModel(modelInputs);
Vergessen Sie nicht, den Speicher unseres tModel-Objekts mit der Funktion DeInit() zu löschen. Die Hauptfunktionalität wird in der Funktion OnTick() ausgeführt, die durch den folgenden Code ergänzt wird.
tModel.Processing(); // place orders MqlDateTime time_curr; // current time structure TimeCurrent(time_curr); // request current time if(time_curr.hour >= 23) // if end of day { RMB.AllOrdersClose(); // close all positions }
Vergleichen wir nun die Ergebnisse der gleichen Strategie mit und ohne Risikokontrollklasse. Führen wir die Einheitstestdatei ManualRiskManager(UniTest1) ohne die Risikokontrollmethode aus. Für den Zeitraum Januar bis März 2024 erhalten wir erhalten das folgende Ergebnis unserer Strategie.
Abbildung 4. Test der Daten, ohne einen Risikomanager zu verwenden
Daraus ergibt sich ein positiver mathematischer Erwartungswert für diese Strategie mit den folgenden Parametern.
# | Name des Parameters | Parameterwert |
---|---|---|
1 | EA | ManualRiskManager(UniTest1) |
2 | Symbol | USDJPY |
3 | Chart-Zeitrahmen | М15 |
4 | Zeitspanne | 2024.01.01 - 2024.03.18 |
5 | Vorwärts-Test | NO |
6 | Verzögerungen | Keine Verzögerungen, perfekte Leistung |
7 | Simulation | Jeder Tick (Every Tick) |
8 | Ursprüngliche Einlage | 10.000 USD |
9 | Hebel | 1:100 |
Tabelle 1. Eingabeparameter für den Strategietester
Führen wir nun die Unit-Test-Datei ManualRiskManager(UniTest2) aus, in der wir unsere Risikomanager-Klasse mit den folgenden Eingabeparametern verwenden.
Name des Eingabeparameters | Variablenwert |
---|---|
inp_riskperday | 0.25 |
inp_riskperweek | 0.75 |
inp_riskpermonth | 2.25 |
inp_plandayprofit | 0.78 |
dayProfitControl | true |
Tabelle 2. Eingabeparameter für den Risikomanager
Die Logik für die Generierung von Eingabeparametern ähnelt der Logik, die oben beim Entwurf der Struktur der Eingabeparameter in Teil 3 beschrieben wurde. Die Gewinnkurve wird wie folgt aussehen.
Abbildung 5. Daten mit einem Risikomanager prüfen
Eine Zusammenfassung der Testergebnisse für die beiden Fälle ist in der folgenden Tabelle dargestellt.
# | Wert | Kein Risikomanager | Risiko-Manager | Ändern Sie |
---|---|---|---|---|
1 | Reingewinn: | 41.1 | 144.48 | +103.38 |
2 | Salden-Drawdown Maximal: | 0.74% | 0.25% | Um das 3fache reduziert |
3 | Kapital-Drawdown Maximal: | 1.13% | 0.58% | Um das 2-fache reduziert |
4 | Expected Payoff: | 10.28 | 36.12 | Mehr als 3-faches Wachstum |
5 | Sharpe Ratio: | 0.12 | 0.67 | 5-faches Wachstum |
6 | Handelsgeschäfte mit Gewinn (in % von allen): | 75% | 75% | - |
7 | Durchschnitt der Handelsgeschäfte mit Gewinn: | 38.52 | 56.65 | Wachstum um 50% |
8 | Durchschnitt der Handelsgeschäfte mit Verlust: | -74.47 | -25.46 | Um das 3-fache reduziert |
9 | Durchschnittliche Risiko-Ertrags-Rate | 0.52 | 2.23 | 4-faches Wachstum |
Tabelle 3. Vergleich der finanziellen Ergebnisse des Handels mit und ohne Risikomanager
Aus den Ergebnissen unserer Einheitstests können wir schließen, dass der Einsatz der Risikokontrolle durch unsere Risikomanagerklasse die Effizienz des Handels mit derselben einfachen Strategie deutlich erhöht hat, indem die Risiken begrenzt und die Gewinne für jede Transaktion im Verhältnis zum festgelegten Risiko festgelegt wurden. Dadurch konnte der Drawdown vom Saldo um das Dreifache und der vom Kapitals um das Zweifache reduziert werden. Der Expected Payoff für die Strategie stieg um mehr als das Dreifache, und die Sharpe Ratio stieg um mehr als das Fünffache. Der durchschnittliche gewinnbringende Handel erhöhte sich um 50 %, und der durchschnittliche unrentable Handel verringerte sich um das Dreifache, was es ermöglichte, die durchschnittliche Risikorendite des Kontos fast auf den Zielwert von 1 zu 3 zu bringen. Die nachstehende Tabelle enthält einen detaillierten Vergleich der Finanzergebnisse für jeden einzelnen Handel aus unserem Pool.
Datum | Symbol | Richtung | Losgröße | Kein Risikomanager | Risiko-Manager | Ändern Sie |
---|---|---|---|---|---|---|
2024.01.31 | USDJPY | buy | 0.1 | 25.75 | 78 | + 52.25 |
2024.02.05 | USDJPY | sell | 0.1 | 13.19 | 13.19 | - |
2024.02.08 | USDJPY | sell | 0.1 | 76.63 | 78.75 | + 2.12 |
2024.02.08 | USDJPY | buy | 0.1 | -74.47 | -25.46 | + 49.01 |
Total | - | - | - | 41.10 | 144.48 | + 103.38 |
Tabelle 4. Vergleich der ausgeführten Handelsgeschäfte mit und ohne Risikomanager
Schlussfolgerung
Auf der Grundlage der in dem Artikel vorgestellten Thesen können folgende Schlussfolgerungen gezogen werden. Der Einsatz des Risikomanagers kann auch beim manuellen Handel die Effektivität von Strategien, auch von profitablen Strategien, deutlich erhöhen. Im Falle einer verlustreichen Strategie kann der Einsatz des Risikomanagers helfen, Einlagen zu sichern und Verluste zu begrenzen. Wie bereits in der Einleitung erwähnt, versuchen wir, den psychologischen Faktor abzumildern. Sie sollten den Risikomanager nicht ausschalten, wenn Sie versuchen, Verluste sofort auszugleichen. Es kann besser sein, den Zeitraum abzuwarten, in dem die Limits erreicht sind, und ohne Emotionen wieder mit dem Handel zu beginnen. Nutzen Sie die Zeit, in der der Risikomanager den Handel untersagt, um Ihre Handelsstrategie zu analysieren und zu verstehen, was zu Verlusten geführt hat und wie diese in Zukunft vermieden werden können.
Vielen Dank an alle, die diesen Artikel bis zum Ende gelesen haben. Ich hoffe wirklich, dass dieser Artikel wenigstens eine Einlage vor dem völligen Verlust bewahrt. In diesem Fall bin ich der Ansicht, dass meine Bemühungen nicht umsonst waren. Ich freue mich über Ihre Kommentare oder privaten Nachrichten, insbesondere darüber, ob ich einen neuen Artikel beginnen sollte, in dem wir diese Klasse an einen rein algorithmischen EA anpassen können. Ihr Feedback ist willkommen. Ich danke Ihnen!
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/14340
- 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.