English Русский Português
preview
Risikomanager für den manuellen Handel

Risikomanager für den manuellen Handel

MetaTrader 5Beispiele | 12 August 2024, 12:03
28 0
Aleksandr Seredin
Aleksandr Seredin

Inhalt


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

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.

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

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. Testdaten ohne Einsatz eines Risikomanagers

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. Testdaten unter Verwendung eines Risikomanagers

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

Die Übertragung der Trading-Signale in einem universalen Expert Advisor. Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
In diesem Artikel wurden die verschiedenen Möglichkeiten beschrieben, um die Trading-Signale von einem Signalmodul des universalen EAs zum Steuermodul der Positionen und Orders zu übertragen. Es wurden die seriellen und parallelen Interfaces betrachtet.
Entwicklung eines Expert Advisors für mehrere Währungen (Teil 5): Variable Positionsgrößen Entwicklung eines Expert Advisors für mehrere Währungen (Teil 5): Variable Positionsgrößen
In den vorangegangenen Teilen konnte der in Entwicklung befindliche Expert Advisor (EA) nur eine feste Positionsgröße für den Handel verwenden. Dies ist für Testzwecke akzeptabel, aber für den Handel mit einem echten Konto nicht ratsam. Lassen Sie uns den Handel mit variablen Positionsgrößen ermöglichen.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
GIT: Was ist das? GIT: Was ist das?
In diesem Artikel werde ich ein sehr wichtiges Werkzeug für Entwickler vorstellen. Wenn Sie mit GIT nicht vertraut sind, lesen Sie diesen Artikel, um eine Vorstellung davon zu bekommen, was es ist und wie man es mit MQL5 verwendet.