Kontinuierliche Rolloptimierung (Teil 2): Mechanismus zur Erstellung eines Optimierungsberichts für einen beliebigen Roboter

20 Februar 2020, 09:55
Andrey Azatskiy
0
264

Einführung

Dies ist der nächste Artikel innerhalb einer Serie, die sich mit der Erstellung eines automatischen Optimierungsprogramms befasst, das die Optimierung von Handelsstrategien während seinen Laufes durchführen kann. Der vorherige Artikel beschrieb die Erstellung einer DLL, die in unserem automatischen Optimierer und in Expert Advisors verwendet werden kann. Dieser neue Teil ist vollständig der Sprache MQL5 gewidmet. Wir werden Methoden zur Erstellung von Optimierungsberichten und die Anwendung dieser Funktionalität innerhalb Ihrer Algorithmen betrachten. 

Der Strategietester erlaubt keinen Zugriff auf seine Daten von einem Expert Advisor aus, solange die bereitgestellten Ergebnisse keine Details enthalten. Daher werden wir die in meinen früheren Artikeln implementierte Funktionalität zum Herunterladen von Optimierungsberichten verwenden. Da einzelne Teile dieser Funktionen modifiziert wurden, während andere in früheren Artikeln nicht vollständig abgedeckt wurden, lassen Sie uns diese Funktionen noch einmal betrachten, da sie die wichtigsten Teile unseres Programms darstellen. Beginnen wir mit einer der neuen Funktionen: die Hinzufügung von kundenspezifischen Provisionen. Alle in diesem Artikel beschriebenen Klassen und Funktionen befinden sich im Verzeichnis Include/History manager.

Implementierung von Sonderprovision und Slippage

Der Tester des MetaTrader 5 Terminals bietet eine Menge aufregender Möglichkeiten. Einige Broker stellen jedoch keine Handelsprovisionen zur Verfügung. Außerdem möchten Sie manchmal zusätzliche Provisionen für zusätzliche Strategietests hinzufügen. Für diese Zwecke habe ich eine Klasse hinzugefügt, die die Provisionen für jedes einzelne Symbol sichert. Nach dem Aufruf einer geeigneten Methode gibt die Klasse Provisionen und das angegebene Slippage zurück. Die Klasse selbst ist wie folgt betitelt:

class CCCM
  {
private:
   struct Keeper
     {
      string            symbol;
      double            comission;
      double            shift;
     };

   Keeper            comission_data[];
public:

   void              add(string symbol,double comission,double shift);

   double            get(string symbol,double price,double volume);
   void              remove(string symbol);
  };

Für diese Klasse wurde die Keeper-Struktur erstellt, die Provision und Slippage für den angegebenen Vermögenswert speichert. Es wurde ein Array erstellt, um alle übergebenen Provisionen und Slippage-Werte zu speichern. Die drei deklarierten Methoden fügen Daten hinzu, empfangen und löschen sie. Die Methode zum Hinzufügen von Werten wird wie folgt implementiert: 

void CCCM::add(string symbol,double comission,double shift)
{
 int s=ArraySize(comission_data);

 for(int i=0;i<s;i++)
   {
    if(comission_data[i].symbol==symbol)
        return;
   }

 ArrayResize(comission_data,s+1,s+1);

 Keeper keeper;
 keeper.symbol=symbol;
 keeper.comission=MathAbs(comission);
 keeper.shift=MathAbs(shift);

 comission_data[s]=keeper;
}

Diese Methode implementiert das Hinzufügen eines neuen Objekts zur Kollektion, nachdem eine vorläufige Prüfung durchgeführt wurde, ob das gleiche Objekt bereits früher hinzugefügt wurde. Bitte beachten Sie, dass Slippage und Provision modulo-Form hinzugefügt wurden. Wenn also alle Kosten zusammengezählt werden, hat das Vorzeichen keinen Einfluss auf die Berechnung. Ein weiterer Punkt, den es zu beachten gilt, sind die Berechnungseinheiten.

  • Provision: Je nach Art des Vermögens kann die Provision in der Gewinnwährung oder als Prozentsatz des gehandelten Volumens hinzugefügt werden.
  • Slippage: wird immer in Punkten angegeben. 

Bitte beachten Sie auch, dass diese Werte nicht pro vollständige Position (d.h. Eröffnung + Schließung), sondern pro Handel addiert werden. Die Position hat also folgenden Wert: n*Provision + n*Slippage, wobei n die Anzahl aller Deals innerhalb einer Position ist.

Die Methode remove löscht den ausgewählten Wert. Der Symbolname wird für den Schlüssel verwendet.

void CCCM::remove(string symbol)
{
 int total=ArraySize(comission_data);
 int ind=-1;
 for(int i=0;i<total;i++)
   {
    if(comission_data[i].symbol==symbol)
      {
       ind=i;
       break;
      }
   }
 if(ind!=-1)
    ArrayRemove(comission_data,ind,1);
}

Wenn das entsprechende Symbol nicht gefunden wird, bricht die Methode ab, ohne einen Wert zu löschen.

Die Methode get wird verwendet, um die gewählte Schicht und Provision zu erhalten. Die Implementierung der Methode ist für verschiedene Arten von Werten unterschiedlich. 

double CCCM::get(string symbol,double price,double volume)
{

 int total=ArraySize(comission_data);
 for(int i=0;i<total;i++)
   {
    if(comission_data[i].symbol==symbol)
      {
       ENUM_SYMBOL_CALC_MODE mode=(ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol,SYMBOL_TRADE_CALC_MODE);

       double shift=comission_data[i].shift*SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE);

       double ans;
       switch(mode)
         {
          case SYMBOL_CALC_MODE_FOREX :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_FUTURES :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_CFD :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_CFDINDEX :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_CFDLEVERAGE :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_EXCH_STOCKS :
            {
             double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
             ans=trading_volume*comission_data[i].comission/100+shift*volume;
            }
          break;
          case SYMBOL_CALC_MODE_EXCH_FUTURES :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          case SYMBOL_CALC_MODE_EXCH_BONDS :
            {
             double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
             ans=trading_volume*comission_data[i].comission/100+shift*volume;
            }
          break;
          case SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX :
            {
             double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
             ans=trading_volume*comission_data[i].comission/100+shift*volume;
            }
          break;
          case SYMBOL_CALC_MODE_EXCH_BONDS_MOEX :
            {
             double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
             ans=trading_volume*comission_data[i].comission/100+shift*volume;
            }
          break;
          case SYMBOL_CALC_MODE_SERV_COLLATERAL :
             ans=(comission_data[i].comission+shift)*volume;
             break;
          default: ans=0; break;
         }

       if(ans!=0)
          return -ans;

      }
   }

 return 0;
}

Suchen wir nach dem angegebenen Symbol im Array. Da für verschiedene Symboltypen unterschiedliche Provisionsberechnungsarten verwendet werden, sind auch die Provisionseinstellungsarten unterschiedlich. So werden z.B. die Provisionen für Aktien und Anleihen als Prozentsatz des Umsatzes festgelegt, während der Umsatz als Produkt der Anzahl der Lose mit der Anzahl der Verträge pro Los und dem Geschäftspreis berechnet wird.

Als Ergebnis erhalten wir den monetären Gegenwert der durchgeführten Operation. Das Ergebnis der Methodenausführung ist immer die Summe der Provision und des Geldverlustes. Der Slippage wird basierend auf dem Tick-Wert berechnet. Außerdem wird die beschriebene Klasse in den nächsten Klassen-Download-Berichten verwendet werden. Die Provisionsparameter für jedes der Werte können fest kodiert oder automatisch von einer Datenbank angefordert werden; alternativ können sie als Input an die EA übergeben werden. In meinen Algorithmen bevorzuge ich die letztere Methode. 

Neuerung in der Klasse CDealHistoryGetter

Die Klassen, die in diesem Teil weiter betrachtet werden, wurden bereits in früheren Artikeln erwähnt. Deshalb werde ich nicht tief in die Details für die zuvor besprochenen Klassen gehen. Ich werde jedoch versuchen, umfassende Beschreibungen für neue Klassen bereitzustellen, da der Schlüsselalgorithmus innerhalb des Algorithmus zum Herunterladen von Handelsberichten die Erstellung des heruntergeladenen Berichts ist.  

Beginnen wir mit der Klasse CDealHistoryGetter, die mit einigen Modifikationen seit dem ersten Artikel verwendet wurde. Der erste Artikel war hauptsächlich der Beschreibung dieser Klasse gewidmet. Die neueste Version ist unten angehängt. Sie enthält einige neue Funktionen und kleinere Korrekturen. Der Mechanismus zum Herunterladen des Berichts in der einfach zu lesenden Form wird im ersten Artikel ausführlich beschrieben. In diesem Artikel werden wir die Hinzufügung von Provision und Slippage zum Bericht ausführlicher betrachten. Gemäß dem OOP-Prinzip, das besagt, dass ein Objekt einen bestimmten Zweck erfüllen muss, wird dieses Objekt erstellt, um alle Arten von Handelsberichtsergebnissen zu erhalten. Es enthält die folgenden 'public' Methoden, die jeweils ihre spezifische Rolle erfüllen:

  • getHistory — diese Methode ermöglicht das Herunterladen der nach Positionen gruppierten Handelshistorie. Wenn wir die Handelshistorie in einem Zyklus mit Standardmethoden ohne Filter herunterladen, erhalten wir die Beschreibung der Geschäfte, die von der DealData-Struktur dargestellt werden: 

struct DealData
  {
   long              ticket;        // Deal ticket
   long              order;         // The number of the order that opened the position
   datetime          DT;            // Position open date
   long              DT_msc;        // Position open date in milliseconds
   ENUM_DEAL_TYPE    type;          // Open position type
   ENUM_DEAL_ENTRY   entry;         // Position entry type
   long              magic;         // Unique position number
   ENUM_DEAL_REASON  reason;        // Order placing reason
   long              ID;            // Position ID
   double            volume;        // Position volume (lots)
   double            price;         // Position entry price
   double            comission;     // Commission paid
   double            swap;          // Swap
   double            profit;        // Profit / loss
   string            symbol;        // Symbol
   string            comment;       // Comment specified when at opening
   string            ID_external;   // External ID
  };

Die empfangenen Daten werden nach der offenen Zeit der Position sortiert und nicht anderweitig gruppiert. Dieser Artikel enthält Beispiele, die zeigen, wie schwierig es ist, den Bericht in dieser Form zu lesen, da es beim Handel mit mehreren Algorithmen zu Verwechslungen zwischen den Geschäften kommen kann. Insbesondere, wenn Techniken zur Positionserhöhung verwendet werden, die zusätzlich einen Vermögenswert gemäß den zugrundeliegenden Algorithmen kaufen oder verkaufen. Als Folge davon erhalten wir einen Großteil der Ein- und Ausstiegsgeschäfte, die nicht das vollständige Bild widerspiegeln.

Unsere Methode gruppiert diese Geschäfte nach Positionen. Obwohl es Verwechslungen mit Aufträgen gibt, eliminieren wir unnötige Geschäfte, die sich nicht auf die analysierte Position beziehen. Das Ergebnis wird als eine Struktur gespeichert, die ein Array aus der oben gezeigten Geschäftsstruktur speichert.  

struct DealKeeper
  {
   DealData          deals[]; /* List of all deals for this position
                              (or several positions in case of position reversal)*/
   string            symbol;  // Symbol
   long              ID;      // ID of the position (s)
   datetime          DT_min;  // Open date (or the date of the very first position)
   datetime          DT_max;  // Close date
  };

Beachten Sie, dass diese Klasse bei der Gruppierung keine Magicnummer berücksichtigt, denn wenn zwei oder mehr Algorithmen an einer Position gehandelt werden, überschneiden sie sich oft. Zumindest eine vollständige Trennung ist an der Moskauer Börse, für die ich hauptsächlich Algorithmen schreibe, technisch unmöglich. Außerdem ist das Tool so konzipiert, dass es Handelsergebnisse oder Test-/Optimierungsergebnisse herunterladen kann. Im ersten Fall reicht eine Statistik über das ausgewählte Symbol, während für den zweiten Fall die Magicnummer keine Rolle spielt, da der Strategietester einen Algorithmus nach dem anderen ausführt.

Die Implementierung des Methodenkerns hat sich seit dem ersten Artikel nicht geändert. Jetzt fügen wir ihm eine kundenspezifische Provision hinzu. Für diese Aufgabe wird die oben besprochene Klasse CCCM als Referenz an den Klassenkonstruktor übergeben und im entsprechenden Feld gespeichert. Dann wird zum Zeitpunkt des Ausfüllens der Struktur DealData, d.h. zum Zeitpunkt des Ausfüllens der Provision, die in der übergebenen CCCM-Klasse gespeicherte kundenspezifische Provision hinzugefügt. 

#ifndef ONLY_CUSTOM_COMISSION
               if(data.comission==0 && comission_manager != NULL)
                 {
                  data.comission=comission_manager.get(data.symbol,data.price,data.volume);
                 }
#else
               data.comission=comission_manager.get(data.symbol,data.price,data.volume);
#endif

Der Auftrag wird direktiv und bedingt hinzugefügt. Wenn wir vor dem Verbinden einer Datei mit dieser Klasse im Roboter den Parameter ONLY_CUSTOM_COMISSION definieren, enthält das Provisionsfeld immer die übergebene Provision anstelle des vom Makler angegebenen Wertes. Wenn dieser Parameter nicht definiert ist, wird die übergebene Provision bedingt hinzugefügt: nur wenn sie der Makler nicht zusammen mit den Preisen zur Verfügung stellt. In allen anderen Fällen wird der Wert der Benutzerprovision ignoriert.

  • getIDArr — gibt ein Array mit den IDs der Positionen zurück, die für alle Symbole während des angeforderten Zeitrahmens geöffnet wurden. Positions-IDs ermöglichen die Kombination aller Geschäfte zu Positionen in unserer Methode. Eigentlich ist dies eine eindeutige Liste des Feldes DealData.ID. 
  • getDealsDetales — die Methode ist ähnlich wie getHistory, liefert jedoch weniger Details. Die Idee der Methode besteht darin, eine Tabelle mit Positionen in einer leicht lesbaren Form bereitzustellen, in der jede Zeile einem bestimmten Geschäft entspricht. Jede Position wird durch die folgende Struktur beschrieben: 
    struct DealDetales
      {
       string            symbol;        // Symbol
       datetime          DT_open;       // Open date
       ENUM_DAY_OF_WEEK  day_open;      // Open day
       datetime          DT_close;      // Cloe date
       ENUM_DAY_OF_WEEK  day_close;     // Close day
       double            volume;        // Volume (lots)
       bool              isLong;        // Long/Short
       double            price_in;      // Position entry price
       double            price_out;     // Position exit price
       double            pl_oneLot;     // Profit / loss is trading one lot
       double            pl_forDeal;    // Real profit/loss taking into account commission
       string            open_comment;  // Comment at the time of opening
       string            close_comment; // Comment at the time of closing
      };
    
    
    Sie stellen eine Tabelle von Positionen dar, die nach dem Schlussdatum der Positionen sortiert sind. Das Array dieser Werte wird zur Berechnung der Koeffizienten in der nächsten Klasse verwendet. Außerdem erhalten wir den abschließenden Testbericht, der auf den präsentierten Daten basiert. Darüber hinaus erstellt der Tester auf der Grundlage dieser Daten das Gewinn- und Verlustdiagramm nach dem Handel.

    Beachten Sie, dass der vom Terminal berechnete Wiederherstellungsfaktor bei weiteren Berechnungen von dem auf der Grundlage der empfangenen Daten berechneten Faktor abweichen wird. Dies ist darauf zurückzuführen, dass die heruntergeladenen Daten zwar korrekt sind und die Berechnungsformeln gleich sind, die Quelldaten jedoch unterschiedlich sind. Der Tester berücksichtigt den Erholungsfaktor entlang der grünen Linie, d.h. durch den detaillierten Bericht, und wir werden Berechnungen in blau durchführen, d.h. nach Daten, die Preisschwankungen im Zeitintervall vom Zeitpunkt der Eröffnung einer Position bis zu ihrer Schließung nicht berücksichtigen.   
  • getBalance — diese Methode ist so konzipiert, dass die Bilanzdaten ohne Berücksichtigung der Handelsoperationen am angegebenen Datum erhalten werden. 
    double CDealHistoryGetter::getBalance(datetime toDate)
      {
       if(HistorySelect(0,(toDate>0 ? toDate : TimeCurrent())))
         {
          int total=HistoryDealsTotal(); // Get the total number of positions
          double balance=0;
          for(int i=0; i<total; i++)
            {
             long ticket=(long)HistoryDealGetTicket(i);
    
             ENUM_DEAL_TYPE dealType=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket,DEAL_TYPE);
             if(dealType==DEAL_TYPE_BALANCE ||
                dealType == DEAL_TYPE_CORRECTION ||
                dealType == DEAL_TYPE_COMMISSION)
               {
                balance+=HistoryDealGetDouble(ticket,DEAL_PROFIT);
    
                if(toDate<=0)
                   break;
               }
            }
          return balance;
         }
       else
          return 0;
      }
    
    

Um die Aufgabe zu erfüllen, wird zunächst die Historie aller Geschäfte vom ersten Zeitintervall bis zum angegebenen angefordert. Danach wird der Saldo in einem Zyklus gespeichert, während alle Ein- und Auszahlungen zum ursprünglichen Saldo hinzugefügt werden, wobei die vom Broker bereitgestellten Provisionen und Korrekturen berücksichtigt werden. Wurde als Eingabe ein Nulldatum übergeben, so wird nur der Saldo vom allerersten Datum angefordert.

  • getBalanceWithPL — die Methode ist ähnlich wie die vorherige, aber zusätzlich zu den Saldenänderungen berücksichtigt sie den Gewinn/Verlust der durchgeführten Operationen, einschließlich der Provisionen nach dem oben genannten Prinzip.

Klasse, die den Optimierungsbericht erstellt — Strukturen, die in den Berechnungen verwendet werden

Eine weitere Klasse, die bereits in früheren Artikeln erwähnt wurde, ist CReportCreator. Sie wurde im Artikel Die 100 besten Durchläufe der Optimierung unter dem Abschnitt "Berechnungen" kurz beschrieben. Nun ist es an der Zeit, eine detailliertere Beschreibung zu geben, da diese Klasse alle Koeffizienten berechnet, auf deren Grundlage der Auto-Optimierer entscheidet, ob diese Kombination von Algorithmus-Parametern den geforderten Kriterien entspricht. 

Lassen Sie uns zunächst die Grundidee des bei der Klassenimplementierung verwendeten Ansatzes beschreiben. Eine ähnliche Klasse mit weniger funktionalen Möglichkeiten wurde in meinem ersten Artikel implementiert. Aber sie war sehr langsam, denn um die nächste Gruppe von angeforderten Parametern oder das nächste Diagramm zu berechnen, musste sie die gesamte Handelshistorie neu herunterladen und durchlaufen. Dies wurde bei jeder Parameteranfrage durchgeführt.

Bei zu vielen Daten kann der Ansatz manchmal mehrere Sekunden dauern. Um die Berechnungen zu beschleunigen, habe ich eine andere Klassenimplementierung verwendet, die zusätzlich wesentlich mehr Daten liefert (auch solche, die in den Standardoptimierungsergebnissen nicht verfügbar sind). Sie werden vielleicht feststellen, dass ähnliche Daten für die Berechnung vieler Koeffizienten, wie z.B. maximaler Gewinn/Verlust oder kumulierter Gewinn/Verlust und ähnliches, benötigt werden.

Wenn wir also die Koeffizienten in einer Schleife berechnen und in den Klassenfeldern speichern, können wir diese Daten weiter für die Berechnung aller anderen Parameter verwenden, in denen diese Daten benötigt werden. Auf diese Weise erhalten wir eine Klasse, die die heruntergeladene Historie einmal durchläuft, alle erforderlichen Parameter berechnet und bis zur nächsten Berechnung speichert. Wenn wir dann den erforderlichen Parameter erhalten müssen, kopiert die Klasse die gespeicherten Daten, anstatt sie neu zu berechnen, was den Vorgang erheblich beschleunigt.

Sehen wir uns nun an, wie die Parameter berechnet werden. Beginnen wir mit den Objekten, die die für weitere Berechnungen verwendeten Daten speichern. Diese Objekte werden als verschachtelte Klassenobjekte erstellt, die im 'private' Bereich deklariert sind. Dies geschieht aus zwei Gründen. Erstens, um ihre Verwendung in anderen Klassen zu verhindern, die diese Funktionen nutzen werden. Die große Anzahl der deklarierten Strukturen und Klassen ist verwirrend: einige von ihnen werden für externe Berechnungen benötigt, andere sind technisch, d.h. sie werden für interne Berechnungen verwendet. Und daher ist der zweite Grund, ihren rein technischen Zweck zu betonen. 

Die Struktur von PL_Keeper:

struct PL_keeper
{
 PLChart_item      PL_total[];
 PLChart_item      PL_oneLot[];
 PLChart_item      PL_Indicative[];
};

Diese Struktur wird für die Speicherung aller möglichen Gewinn- und Verlustgrafiken erstellt. Sie wurden in meinem ersten Artikel ausführlich beschrieben (siehe den Link oben). Unterhalb der Strukturdeklaration werden ihre Instanzen erstellt:

PL_keeper         PL,PL_hist,BH,BH_hist;

Jede Instanz speichert 4 präsentierte Diagrammtypen für verschiedene Quelldaten. Daten mit dem PL-Präfix werden auf der Grundlage der oben erwähnten blauen Linie des im Terminal verfügbaren PL-Diagramms berechnet. Daten mit dem BH-Präfix werden auf der Grundlage des Gewinn- und Verlustdiagramms berechnet, das durch die Buy-and-Hold-Strategie erhalten wird. Daten mit dem Postfix 'hist' werden auf der Grundlage des Gewinn- und Verlust-Histogramms berechnet.

Die Struktur DailyPL_keeper:

// The structure of Daily PL graphs
struct DailyPL_keeper
{
 DailyPL           avarage_open,avarage_close,absolute_open,absolute_close;
};

Diese Struktur speichert vier mögliche tägliche Gewinn-/Verlustdiagrammtypen. Die Struktur DailyPL mit dem Präfix 'average' werden anhand der durchschnittlichen Gewinn-/Verlustdaten berechnet. Diejenigen mit dem Präfix 'absolute' verwenden Gesamtgewinn- und -verlustwerte. Dementsprechend sind die Unterschiede zwischen ihnen offensichtlich. Im ersten Fall spiegelt sie den durchschnittlichen Tagesgewinn für die gesamte Handelsperiode wider, im zweiten Fall wird der Gesamtgewinn angezeigt. Die Daten mit dem Präfix 'open' sind nach Tagen nach ihrem Eröffnungsdatum sortiert, während die Daten mit dem Präfix 'close' nach ihrem Abschlussdatum sortiert sind. Die Deklaration der Strukturinstanz wird im folgenden Code angezeigt.

Der RationTable_keeper:

// Table structure of extreme points
struct RatioTable_keeper
  {
   ProfitDrawdown    Total_max,Total_absolute,Total_percent;
   ProfitDrawdown    OneLot_max,OneLot_absolute,OneLot_percent;
  };

Diese Struktur besteht aus Instanzen der Struktur ProfitDrawdown.

struct ProfitDrawdown
  {
   double            Profit; // In some cases Profit, in other Profit / Loss
   double            Drawdown; // Drawdown
  };

Es speichert die Gewinn- und Verlustquote nach bestimmten Kriterien. Daten mit dem Präfix 'Total' werden anhand der Gewinn-/Verlustdiagrammerstellung unter Berücksichtigung von Losänderungen berechnet. Daten mit dem Präfix 'OneLot' werden so berechnet, als ob ein Lot ständig gehandelt würde. Die nicht standardmäßige Idee der One-Lot-Berechnung wird im oben genannten ersten Artikel beschrieben. Kurz gesagt, diese Methode wurde geschaffen, um die Ergebnisse des Handelssystems zu bewerten. Sie ermöglicht es zu erkennen, woher das beste Ergebnis kommt: zeitnahe Losverwaltung oder aus der Logik des Systems selbst. Der Postfix 'max' zeigt, dass die Instanz Daten über den höchsten Gewinn und die höchste Inanspruchnahme, die während der Handelsgeschichte aufgetreten sind, enthält. Der Postfix 'absolute' bedeutet, dass die Instanz Daten über den gesamten Gewinn und die Inanspruchnahme für die gesamte Handelshistorie enthält. Der Postfix 'percent' bedeutet, dass die Gewinn- und Drawdown-Werte als prozentuales Verhältnis zum maximalen Wert auf der PL-Kurve innerhalb des getesteten Zeitrahmens berechnet werden. Die Strukturdeklaration ist einfach und wird in dem dem Artikel beigefügten Code angezeigt.

Die nächste Gruppe von Strukturen wird nicht als Klassenfeld deklariert, sondern wird als lokale Deklaration in der Hauptmethode Create verwendet. Alle beschriebenen Strukturen sind miteinander kombiniert, so dass wir uns die Deklaration aller Strukturen ansehen können. 

// Structures for calculating consecutive profits and losses
   struct S_dealsCounter
     {
      int               Profit,DD;
     };
   struct S_dealsInARow : public S_dealsCounter
     {
      S_dealsCounter    Counter;
     };
   // Struktur zur Berechnung der Hilfsdaten
   struct CalculationData_item
     {
      S_dealsInARow     dealsCounter;
      int               R_arr[];
      double            DD_percent;
      double            Accomulated_DD,Accomulated_Profit;
      double            PL;
      double            Max_DD_forDeal,Max_Profit_forDeal;
      double            Max_DD_byPL,Max_Profit_byPL;
      datetime          DT_Max_DD_byPL,DT_Max_Profit_byPL;
      datetime          DT_Max_DD_forDeal,DT_Max_Profit_forDeal;
      int               Total_DD_numDeals,Total_Profit_numDeals;
     };
   struct CalculationData
     {
      CalculationData_item total,oneLot;
      int               num_deals;
      bool              isNot_firstDeal;
     };


Die Strukturen S_dealsCounter und S_dealsInARow sind im Wesentlichen eine Einheit. Eine solch merkwürdige Kombination aus Assoziation und Vererbung ist gleichzeitig mit der spezifischen Berechnung ihrer Parameter verbunden. Die Struktur S_dealsInARow wird für die Speicherung und Berechnung der Anzahl der Geschäfte (eigentlich für die Berechnung der Positionen, d.h. von der Positionseröffnung bis zur Schließung) in einer Reihe, entweder positiv oder negativ, erstellt. Die geschachtelte Instanz der Struktur S_dealsCounter wird für die Speicherung von Zwischenberechnungsergebnissen deklariert und vererbt Felder behalten ihre Summen bei. Wir werden später auf die Operation zum Zählen von profitablen/verlustbehafteten Geschäften zurückkommen.     

Die Struktur CalculationData_item enthält Felder, die für die Berechnung von Koeffizienten erforderlich sind. 

  • R_arr — Reihe von aufeinanderfolgenden profitablen/verlustbehafteten Geschäften, jeweils als 1 / 0 dargestellt. Das Array wird für die Berechnung von Z-Score verwendet.
  • DD_percent — Prozentsatz des Drawdowns.
  • Accomulated_DD, Accomulated_Profit — Gesamtverlust- und Gewinnwerte speichern.
  • PL - Gewinn / Verlust.
  • Max_DD_forDeal, Max_Profit_forDeal — wie der Name schon sagt, speichern sie die maximale Drawdown und den Gewinn unter allen Geschäften.
  • Max_DD_byPL, Mаx_Profit_byPL — speichern den maximale Drawdown und den durch das PL-Diagramm berechneten Gewinn. 
  • DT_Max_DD_byPL, DT_Max_Profit_byPL — speichern die Daten des höchsten Drawdowns und des Gewinns nach PL-Grafik. 
  • DT_Max_DD_forDeal, DT_Max_Profit_forDeal — Daten des höchsten Drawdowns und des Gewinns nach Geschäften.
  • Total_DD_numDeals, TotalProfit_numDeals — Gesamtzahl der gewinnbringenden und verlustbringenden Geschäfte. 

Weitere Berechnungen basieren auf den oben genannten Daten.

CalculationData ist eine akkumulierende Struktur, die alle beschriebenen Strukturen kombiniert. Sie speichert alle erforderlichen Daten. Sie enthält auch das Feld num_deals, das eigentlich die Summe von CalculationData_item::Total_DD_numDeals und CalculationData_item::TotalProfit_numDeals ist. Das Feld sNot_firstDeal ist ein technisches Flag, das angibt, dass die Berechnung nicht für die allererste Transaktion durchgeführt wird.

Die Struktur CoefChart_keeper:

struct CoefChart_keeper
     {
      CoefChart_item    OneLot_ShartRatio_chart[],Total_ShartRatio_chart[];
      CoefChart_item    OneLot_WinCoef_chart[],Total_WinCoef_chart[];
      CoefChart_item    OneLot_RecoveryFactor_chart[],Total_RecoveryFactor_chart[];
      CoefChart_item    OneLot_ProfitFactor_chart[],Total_ProfitFactor_chart[];
      CoefChart_item    OneLot_AltmanZScore_chart[],Total_AltmanZScore_chart[];
     };

Es ist beabsichtigt, Koeffizientendiagramme zu speichern. Da die Klasse nicht nur Gewinn- und Losdiagramme, sondern auch einige Koeffizientendiagramme erstellt, wurde eine weitere Struktur für die beschriebenen Datentypen erstellt. Das Präfix 'OneLot' zeigt an, dass die Instanz die von der Gewinn-/Verlustanalyse erhaltenen Daten speichern wird, wenn ein Lot gehandelt wird. 'Total' bedeutet Berechnung unter Berücksichtigung des Losgrößen-Managements. Wenn in der Strategie kein Losgrößen-Management verwendet wird, sind die beiden Charts identisch.

The СHistoryComparer class:

In ähnlicher Weise wird eine Klasse definiert, die bei der Datensortierung verwendet wird. Der Artikel "Die 100 besten Durchläufe der Optimierung" enthält die Beschreibung der Klasse CGenericSorter, die jeden Datentyp in absteigender und aufsteigender Reihenfolge sortieren kann. Sie benötigt zusätzlich eine Klasse, die übergebene Typen vergleichen kann. Eine solche Klasse ist СHisoryComparer.

class CHistoryComparer : public ICustomComparer<DealDetales>
     {
   public:
      int               Compare(DealDetales &x,DealDetales &y);
     };

Die Implementierung der Methode ist einfach: Sie vergleicht die Schlussdaten, da die Sortierung nach Abschlussdaten erfolgt:

int CReportCreator::CHistoryComparer::Compare(DealDetales &x,DealDetales &y)
  {
   return(x.DT_close == y.DT_close ? 0 : (x.DT_close > y.DT_close ? 1 : -1));
  }

Es gibt auch eine ähnliche Klasse für die Sortierung von Koeffiziententabellen. Diese beiden Klassen und die Sortierklasse werden als globales Feld der beschriebenen Klasse CReportCreator instanziiert. Zusätzlich zu den beschriebenen Objekten gibt es zwei weitere Felder. Ihre Typen werden als separate, nicht verschachtelte Objekte beschrieben:

PL_detales        PL_detales_data;
DistributionChart OneLot_PDF_chart,Total_PDF_chart;

Die Struktur PL_detales enthält kurze Handelsinformationen für gewinnbringende und verlierende Positionen:

//+------------------------------------------------------------------+
struct PL_detales_PLDD
  {
   int               orders; // Number of deals
   double            orders_in_Percent; // Number of orders as % of total number of orders
   int               dealsInARow; // Deals in a row
   double            totalResult; // Total result in money
   double            averageResult; // Average result in money
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
struct PL_detales_item
  {
   PL_detales_PLDD   profit; // Information on profitable deals
   PL_detales_PLDD   drawdown; // Information on losing deals
  };
//+-------------------------------------------------------------------+
//| Ein kurzes Gewinn- und Verlustdiagramm in zwei Hauptblöcken       |
//+-------------------------------------------------------------------+
struct PL_detales
  {
   PL_detales_item   total,oneLot;
  };

Das zweite Struktur DistributionChart enthält eine Reihe von VaR-Werten sowie das Verteilungsdiagramm, auf dessen Grundlage diese Koeffizienten berechnet wurden. Die Verteilung wird als Normalverteilung berechnet.

//+------------------------------------------------------------------+
//| Structure used for saving distribution charts                    |
//+------------------------------------------------------------------+
struct Chart_item
  {
   double            y; // y axis
   double            x; // x axis
  };
//+------------------------------------------------------------------+
//| Structure contains the VaR value                                 |
//+------------------------------------------------------------------+
struct VAR
  {
   double            VAR_90,VAR_95,VAR_99;
   double            Mx,Std;
  };
//+------------------------------------------------------------------+
//| Structure - it is used to store distribution charts and          |
//| the VaR values                                                   |
//+------------------------------------------------------------------+
struct Distribution_item
  {
   Chart_item        distribution[]; // Distribution chart
   VAR               VaR; // VaR
  };
//+------------------------------------------------------------------+
//| Structure - Stores distribution data. Divided into 2 blocks      |
//+------------------------------------------------------------------+
struct DistributionChart
  {
   Distribution_item absolute,growth;
  };

Die VaR-Koeffizienten werden nach einer Formel berechnet: Historischer VaR, die vielleicht nicht genau genug ist, aber für die aktuelle Umsetzung durchaus geeignet ist. 

Methoden zur Berechnung von Koeffizienten, die die Handelsergebnisse beschreiben

Nachdem wir nun die Datenspeicherstrukturen betrachtet haben, können Sie sich die riesige Menge an Statistiken vorstellen, die von dieser Klasse berechnet werden. Betrachten wir die spezifischen Methoden zur Berechnung der beschriebenen Werte einzeln, wie sie in der Klasse CReportCreator genannt werden.

Die CalcPL wird zur Berechnung des PL-Graphen erstellt. Sie ist wie folgt implementiert:

void CReportCreator::CalcPL(const DealDetales &deal,CalculationData &data,PLChart_item &pl_out[],CalcType type)
  {
   PLChart_item item;
   ZeroMemory(item);
   item.DT=deal.DT_close; // Saving the date

   if(type!=_Indicative)
     {
      item.Profit=(type==_Total ? data.total.PL : data.oneLot.PL); // Saving the profit
      item.Drawdown=(type==_Total ? data.total.DD_percent : data.oneLot.DD_percent); // Saving the drawdown
     }
   else // Calculating the indicative chart
     {
      if(data.isNot_firstDeal)
        {
         if(data.total.PL!=0)
           {
            if(data.total.PL > 0 && data.total.Max_DD_forDeal < 0)
               item.Profit=data.total.PL/MathAbs(data.total.Max_DD_forDeal);
            else
               if(data.total.PL<0 && data.total.Max_Profit_forDeal>0)
                  item.Profit=data.total.PL/data.total.Max_Profit_forDeal;
           }
        }
     }
// Adding data to array
   int s=ArraySize(pl_out);
   ArrayResize(pl_out,s+1,s+1);
   pl_out[s]=item;
  }

Wie aus der Implementierung ersichtlich, basieren alle seine Berechnungen auf Daten der zuvor beschriebenen Strukturen, die als Eingabe übergeben werden.

Wenn Sie ein nicht indikatives PL-Diagramm berechnen müssen, kopieren Sie einfach die bekannten Daten. Andernfalls unterliegt die Berechnung zwei Bedingungen: die erste Iteration wurde im Zyklus nicht gefunden und das PL-Diagramm ist ungleich Null. Die Berechnung wird nach folgender Logik durchgeführt:

  • Wenn der PL-Wert größer als Null ist und der Drawdown kleiner ist, dividieren Sie den aktuellen PL-Wert durch den Wert des Drawdowns. So erhalten wir einen Koeffizienten, der angibt, wie viele aufeinanderfolgende maximale Absenkungen erforderlich sind, um den aktuellen PL auf Null zu reduzieren. 
  • Wenn der PL-Wert kleiner als Null ist und der maximale Gewinn für alle Geschäfte größer als Null ist, dann dividieren wir den PL-Wert (der derzeit der Drawdown-Wert ist) durch den erzielten maximalen Gewinn. Auf diese Weise erhalten wir einen Koeffizienten, der angibt, wie viele maximale Gewinne in einer Reihe erforderlich wären, um den aktuellen Drawdown auf Null zu bringen.

Die nächste Methode CalcPLHist basiert auf einem ähnlichen Mechanismus, aber sie verwendet andere Strukturfelder zur Berechnung: data.oneLot.Accomulated_DD, data.total.Accomulated_DD and data.oneLot.Accomulated_Profit, data.total.Accomulated_Profit. Wir haben den Algorithmus bereits früher besprochen, daher gehen wir zu den nächsten beiden Methoden über.

CalcData und CalcData_item:

Diese Methoden berechnen alle Hilfs- und Hauptkoeffizienten. Beginnen wir mit dem CalcData_item. Sein Zweck ist die Berechnung der oben beschriebenen Zusatzkoeffizienten, auf deren Grundlage die Hauptkoeffizienten berechnet werden.  

//+------------------------------------------------------------------+
//| Calculating auxiliary data                                       |
//+------------------------------------------------------------------+
void CReportCreator::CalcData_item(const DealDetales &deal,CalculationData_item &out,
                                   bool isOneLot)
  {
   double pl=(isOneLot ? deal.pl_oneLot : deal.pl_forDeal); //PL
   int n=0;
// Number of profits and losses
   if(pl>=0)
     {
      out.Total_Profit_numDeals++;
      n=1;
      out.dealsCounter.Counter.DD=0;
      out.dealsCounter.Counter.Profit++;
     }
   else
     {
      out.Total_DD_numDeals++;
      out.dealsCounter.Counter.DD++;
      out.dealsCounter.Counter.Profit=0;
     }
   out.dealsCounter.DD=MathMax(out.dealsCounter.DD,out.dealsCounter.Counter.DD);
   out.dealsCounter.Profit=MathMax(out.dealsCounter.Profit,out.dealsCounter.Counter.Profit);

// Series of profits and losses
   int s=ArraySize(out.R_arr);
   if(!(s>0 && out.R_arr[s-1]==n))
     {
      ArrayResize(out.R_arr,s+1,s+1);
      out.R_arr[s]=n;
     }

   out.PL+=pl; //Total PL
// Max Profit / DD
   if(out.Max_DD_forDeal>pl)
     {
      out.Max_DD_forDeal=pl;
      out.DT_Max_DD_forDeal=deal.DT_close;
     }
   if(out.Max_Profit_forDeal<pl)
     {
      out.Max_Profit_forDeal=pl;
      out.DT_Max_Profit_forDeal=deal.DT_close;
     }
// Akkumulierter Profit / DD
   out.Accomulated_DD+=(pl>0 ? 0 : pl);
   out.Accomulated_Profit+=(pl>0 ? pl : 0);
// Extreme profit values
   double maxPL=MathMax(out.Max_Profit_byPL,out.PL);
   if(compareDouble(maxPL,out.Max_Profit_byPL)==1/* || !isNot_firstDeal*/)// another check is needed to save the date
     {
      out.DT_Max_Profit_byPL=deal.DT_close;
      out.Max_Profit_byPL=maxPL;
     }
   double maxDD=out.Max_DD_byPL;
   double DD=0;
   if(out.PL>0)
      DD=out.PL-maxPL;
   else
      DD=-(MathAbs(out.PL)+maxPL);
   maxDD=MathMin(maxDD,DD);
   if(compareDouble(maxDD,out.Max_DD_byPL)==-1/* || !isNot_firstDeal*/)// another check is needed to save the date
     {
      out.Max_DD_byPL=maxDD;
      out.DT_Max_DD_byPL=deal.DT_close;
     }
   out.DD_percent=(balance>0 ?(MathAbs(DD)/(maxPL>0 ? maxPL : balance)) :(maxPL>0 ?(MathAbs(DD)/maxPL) : 0));
  }

Zunächst wird PL bei der i-ten Iteration berechnet. Dann, wenn bei dieser Iteration ein Gewinn erzielt wurde, wird der Zähler des profitablen Geschäfts erhöht und der Zähler der nachfolgenden Verluste auf Null gesetzt. Außerdem wird der Wert 1 für die n-Variable gesetzt, was bedeutet, dass das Geschäft profitabel war. Wenn PL unter Null lag, wird der Verlustzähler und der Zähler der profitablen Geschäfte auf Null gesetzt. Danach wird die maximale Anzahl von profitablen und verlustreichen Serien in einer Reihe zugewiesen.

Der nächste Schritt ist die Berechnung der Reihe von profitablen und verlorenen Geschäften. Eine Serie bedeutet aufeinanderfolgende Gewinn- oder Verlustpositionen. In dieser Anordnung folgt auf eine Null immer eine Eins, während auf die Eins immer eine Null folgt. Dies zeigt den Wechsel von gewinnenden und verlierenden Geschäften, wobei 0 oder 1 jedoch mehrere Geschäfte bedeuten können. Dieses Array wird zur Berechnung des Z-Scores verwendet, der den Grad der Handelszufälligkeit anzeigt. Der nächste Schritt ist die Zuweisung von Maximalwerten von Gewinn und Verlust und den kumulierten Gewinn/Verlust berechnen. Am Ende dieser Methode werden Extrempunkte berechnet, d.h. es werden Strukturen mit den maximalen Gewinn- und Verlustwerten gefüllt.

Die Daten von CalcData verwenden bereits die erhaltenen Zwischendaten zur Berechnung der erforderlichen Koeffizienten und aktualisieren die Berechnungen bei jeder Iteration. Sie ist wie folgt implementiert:

void CReportCreator::CalcData(const DealDetales &deal,CalculationData &out,bool isBH)
  {
   out.num_deals++; // Counting the number of deals
   CalcData_item(deal,out.oneLot,true);
   CalcData_item(deal,out.total,false);

   if(!isBH)
     {
      // Fill PL graphs
      CalcPL(deal,out,PL.PL_total,_Total);
      CalcPL(deal,out,PL.PL_oneLot,_OneLot);
      CalcPL(deal,out,PL.PL_Indicative,_Indicative);

      // Fill PL Histogram graphs
      CalcPLHist(deal,out,PL_hist.PL_total,_Total);
      CalcPLHist(deal,out,PL_hist.PL_oneLot,_OneLot);
      CalcPLHist(deal,out,PL_hist.PL_Indicative,_Indicative);

      // Fill PL graphs by days
      CalcDailyPL(DailyPL_data.absolute_close,CALC_FOR_CLOSE,deal);
      CalcDailyPL(DailyPL_data.absolute_open,CALC_FOR_OPEN,deal);
      CalcDailyPL(DailyPL_data.avarage_close,CALC_FOR_CLOSE,deal);
      CalcDailyPL(DailyPL_data.avarage_open,CALC_FOR_OPEN,deal);

      // Fill Profit Factor graphs
      ProfitFactor_chart_calc(CoefChart_data.OneLot_ProfitFactor_chart,out,deal,true);
      ProfitFactor_chart_calc(CoefChart_data.Total_ProfitFactor_chart,out,deal,false);

      // Fill Recovery Factor graphs
      RecoveryFactor_chart_calc(CoefChart_data.OneLot_RecoveryFactor_chart,out,deal,true);
      RecoveryFactor_chart_calc(CoefChart_data.Total_RecoveryFactor_chart,out,deal,false);

      // Fill winning coefficient graphs
      WinCoef_chart_calc(CoefChart_data.OneLot_WinCoef_chart,out,deal,true);
      WinCoef_chart_calc(CoefChart_data.Total_WinCoef_chart,out,deal,false);

      // Fill Sharpe Ration graphs
      ShartRatio_chart_calc(CoefChart_data.OneLot_ShartRatio_chart,PL.PL_oneLot,deal/*,out.isNot_firstDeal*/);
      ShartRatio_chart_calc(CoefChart_data.Total_ShartRatio_chart,PL.PL_total,deal/*,out.isNot_firstDeal*/);

      // Fill Z Score graphs
      AltmanZScore_chart_calc(CoefChart_data.OneLot_AltmanZScore_chart,(double)out.num_deals,
                              (double)ArraySize(out.oneLot.R_arr),(double)out.oneLot.Total_Profit_numDeals,
                              (double)out.oneLot.Total_DD_numDeals/*,out.isNot_firstDeal*/,deal);
      AltmanZScore_chart_calc(CoefChart_data.Total_AltmanZScore_chart,(double)out.num_deals,
                              (double)ArraySize(out.total.R_arr),(double)out.total.Total_Profit_numDeals,
                              (double)out.total.Total_DD_numDeals/*,out.isNot_firstDeal*/,deal);
     }
   else // Fill PL Buy and Hold graphs
     {
      CalcPL(deal,out,BH.PL_total,_Total);
      CalcPL(deal,out,BH.PL_oneLot,_OneLot);
      CalcPL(deal,out,BH.PL_Indicative,_Indicative);

      CalcPLHist(deal,out,BH_hist.PL_total,_Total);
      CalcPLHist(deal,out,BH_hist.PL_oneLot,_OneLot);
      CalcPLHist(deal,out,BH_hist.PL_Indicative,_Indicative);
     }

   if(!out.isNot_firstDeal)
      out.isNot_firstDeal=true; // Flag "It is NOT the first deal"
  }

Zuerst werden Zwischenkoeffizienten für ein Los und Handelssysteme mit gemanagten Losgrößen, indem die beschriebene Methode für beide Datentypen aufgerufen wird. Dann wird die Berechnung in Koeffizienten für BH und Daten des entgegengesetzten Typs aufgeteilt. Die interpretierbaren Koeffizienten werden innerhalb jedes Blocks berechnet. Für die Strategie Kaufen und Halten werden nur Graphen berechnet, und daher werden keine Koeffizientenberechnungsmethoden aufgerufen.  

Die nächste Gruppe von Methoden berechnet den Gewinn/Verlust geteilt durch Tage:

//+------------------------------------------------------------------+
//| Create a structure of trading during a day                       |
//+------------------------------------------------------------------+
void CReportCreator::CalcDailyPL(DailyPL &out,DailyPL_calcBy calcBy,const DealDetales &deal)
  {
   cmpDay(deal,MONDAY,out.Mn,calcBy);
   cmpDay(deal,TUESDAY,out.Tu,calcBy);
   cmpDay(deal,WEDNESDAY,out.We,calcBy);
   cmpDay(deal,THURSDAY,out.Th,calcBy);
   cmpDay(deal,FRIDAY,out.Fr,calcBy);
  }
//+------------------------------------------------------------------+
//| Save resulting PL/DD for the day                                 |
//+------------------------------------------------------------------+
void CReportCreator::cmpDay(const DealDetales &deal,ENUM_DAY_OF_WEEK etalone,PLDrawdown &ans,DailyPL_calcBy calcBy)
  {
   ENUM_DAY_OF_WEEK day=(calcBy==CALC_FOR_CLOSE ? deal.day_close : deal.day_open);
   if(day==etalone)
     {
      if(deal.pl_forDeal>0)
        {
         ans.Profit+=deal.pl_forDeal;
         ans.numTrades_profit++;
        }
      else
         if(deal.pl_forDeal<0)
           {
            ans.Drawdown+=MathAbs(deal.pl_forDeal);
            ans.numTrades_drawdown++;
           }
     }
  }
//+------------------------------------------------------------------+
//| Average resulting PL/DD for the day                              |
//+------------------------------------------------------------------+
void CReportCreator::avarageDay(PLDrawdown &day)
  {
   if(day.numTrades_profit>0)
      day.Profit/=day.numTrades_profit;
   if(day.numTrades_drawdown > 0)
      day.Drawdown/=day.numTrades_drawdown;
  }



Die Hauptarbeit, die den Gewinn/DD nach Tagen aufteilt, wird in der cmpDay-Methode durchgeführt, bei der zunächst geprüft wird, ob der Tag dem gewünschten Tag entspricht oder nicht, und dann Gewinn- und Verlustwerte addiert werden. Verluste werden als Modulo summiert. CalcDailyPL ist eine Aggregationsmethode, bei der versucht wird, den aktuell übergebenen PL zu einem der fünf Arbeitstage zu addieren. Die Durchschnittsmethode wird in der Hauptmethode Create aufgerufen, um Gewinne/Verluste zu mitteln. Diese Methode führt keine spezifischen Aktionen aus, während sie nur den Durchschnitt auf der Grundlage der zuvor berechneten absoluten Gewinn-/Verlustwerte berechnet. 

Methode zur Berechnung des Profit Factors

//+------------------------------------------------------------------+
//| Calculate Profit Factor                                          |
//+------------------------------------------------------------------+
void CReportCreator::ProfitFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot)
  {
   CoefChart_item item;
   item.DT=deal.DT_close;
   double profit=(isOneLot ? data.oneLot.Accomulated_Profit : data.total.Accomulated_Profit);
   double dd=MathAbs(isOneLot ? data.oneLot.Accomulated_DD : data.total.Accomulated_DD);
   if(dd==0)
      item.coef=0;
   else
      item.coef=profit/dd;
   int s=ArraySize(out);
   ArrayResize(out,s+1,s+1);
   out[s]=item;
  }

Die Methode berechnet ein Diagramm, das die Veränderung des Gewinnfaktors während des gesamten Handels widerspiegelt. Der allerletzte Wert ist derjenige, der im Testbericht angezeigt wird. Die Formel ist einfach = akkumuliert Gewinn / kumulierter Drawdown. Wenn der Drawdown gleich Null ist, dann ist der Koeffizient gleich Null, da in der klassischen Arithmetik eine Division durch Null ohne Verwendung von Grenzen unmöglich ist, und dieselbe Regel gilt auch in der Sprache. Daher werden wir für alle arithmetischen Operationen den Divisor auf Null prüfen.

Das Prinzip der Berechnung des Wiederherstellungsfaktors ist ähnlich:

//+------------------------------------------------------------------+
//| Calculate Recovery Factor                                        |
//+------------------------------------------------------------------+
void CReportCreator::RecoveryFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot)
  {
   CoefChart_item item;
   item.DT=deal.DT_close;
   double pl=(isOneLot ? data.oneLot.PL : data.total.PL);
   double dd=MathAbs(isOneLot ? data.oneLot.Max_DD_byPL : data.total.Max_DD_byPL);
   if(dd==0)
      item.coef=0;//ideally it should be plus infinity
   else
      item.coef=pl/dd;
   int s=ArraySize(out);
   ArrayResize(out,s+1,s+1);
   out[s]=item;
  }

Formel zur Koeffizientenberechnung: Gewinn wie bei der i-ten Iteration / Drawdown wie bei der i-ten Iteration. Beachten Sie auch, dass der Gewinn während der Koeffizientenberechnung Null oder negativ sein kann, auch der Koeffizient selbst kann null oder negativ sein.

Gewinnrate

//+------------------------------------------------------------------+
//| Calculate Win Rate                                               |
//+------------------------------------------------------------------+
void CReportCreator::WinCoef_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot)
  {
   CoefChart_item item;
   item.DT=deal.DT_close;
   double profit=(isOneLot ? data.oneLot.Accomulated_Profit : data.total.Accomulated_Profit);
   double dd=MathAbs(isOneLot ? data.oneLot.Accomulated_DD : data.total.Accomulated_DD);
   int n_profit=(isOneLot ? data.oneLot.Total_Profit_numDeals : data.total.Total_Profit_numDeals);
   int n_dd=(isOneLot ? data.oneLot.Total_DD_numDeals : data.total.Total_DD_numDeals);
   if(n_dd == 0 || n_profit == 0)
      item.coef = 0;
   else
      item.coef=(profit/n_profit)/(dd/n_dd);
   int s=ArraySize(out);
   ArrayResize(out,s+1,s+1);
   out[s]=item;
  }

Berechnungsformel der Gewinnrate = (Gewinn / Anzahl der Gewinn-Positionen) / (Drawdown / Anzahl der Verlust-Position). Dieser Koeffizient kann auch negativ sein, wenn zum Zeitpunkt der Berechnung kein Gewinn vorliegt. 

Die Berechnung der Sharpe-Ratio ist etwas komplizierter:

//+------------------------------------------------------------------+
//| Calculate Sharpe Ratio                                           |
//+------------------------------------------------------------------+
double CReportCreator::ShartRatio_calc(PLChart_item &data[])
  {
   int total=ArraySize(data);
   double ans=0;
   if(total>=2)
     {
      double pl_r=0;
      int n=0;
      for(int i=1; i<total; i++)
        {
         if(data[i-1].Profit!=0)
           {
            pl_r+=(data[i].Profit-data[i-1].Profit)/data[i-1].Profit;
            n++;
           }
        }
      if(n>=2)
         pl_r/=(double)n;
      double std=0;
      n=0;
      for(int i=1; i<total; i++)
        {
         if(data[i-1].Profit!=0)
           {
            std+=MathPow((data[i].Profit-data[i-1].Profit)/data[i-1].Profit-pl_r,2);
            n++;
           }
        }
      if(n>=2)
         std=MathSqrt(std/(double)(n-1));

      ans=(std!=0 ?(pl_r-r)/std : 0);
     }
   return ans;
  }

Im ersten Zyklus wird eine durchschnittliche Rentabilität durch das PL-Diagramm berechnet, in dem jede i-te Rentabilität als Verhältnis des Anstiegs über PL zum vorherigen PL-Wert berechnet wird. Die Berechnung basiert auf dem Beispiel der Preisreihennormalisierung, die für die Auswertung von Zeitreihen verwendet wird. 

Im nächsten Zyklus wird die Volatilität unter Verwendung derselben normalisierten Rentabilitätsreihe berechnet.

Danach wird der Koeffizient selbst mit der Formel (durchschnittlicher Gewinn - risikofreie Rate) / Volatilität (Standardabweichung der Erträge) berechnet.

Vielleicht habe ich bei der Reihennormalisierung einen nicht-traditionellen Ansatz und wahrscheinlich sogar die Formel angewandt, aber diese Berechnung scheint ziemlich vernünftig zu sein. Wenn Sie einen Fehler finden, fügen Sie bitte einen Kommentar zu dem Artikel hinzu.

Berechnung von VaR und Normalverteilungsdiagramm. Dieser Teil besteht aus drei Methoden. Zwei davon sind Berechnungen, die dritte aggregiert alle Berechnungen. Lassen Sie uns diese Methoden betrachten.

//+------------------------------------------------------------------+
//| Distribution calculation                                         |
//+------------------------------------------------------------------+
void CReportCreator::NormalPDF_chart_calc(DistributionChart &out,PLChart_item &data[])
  {
   double Mx_absolute=0,Mx_growth=0,Std_absolute=0,Std_growth=0;
   int total=ArraySize(data);
   ZeroMemory(out.absolute);
   ZeroMemory(out.growth);
   ZeroMemory(out.absolute.VaR);
   ZeroMemory(out.growth.VaR);
   ArrayFree(out.absolute.distribution);
   ArrayFree(out.growth.distribution);

// Calculation of distribution parameters
   if(total>=2)
     {
      int n=0;
      for(int i=0; i<total; i++)
        {
         Mx_absolute+=data[i].Profit;
         if(i>0 && data[i-1].Profit!=0)
           {
            Mx_growth+=(data[i].Profit-data[i-1].Profit)/data[i-1].Profit;
            n++;
           }
        }
      Mx_absolute/=(double)total;
      if(n>=2)
         Mx_growth/=(double)n;

      n=0;
      for(int i=0; i<total; i++)
        {
         Std_absolute+=MathPow(data[i].Profit-Mx_absolute,2);
         if(i>0 && data[i-1].Profit!=0)
           {
            Std_growth+=MathPow((data[i].Profit-data[i-1].Profit)/data[i-1].Profit-Mx_growth,2);
            n++;
           }
        }
      Std_absolute=MathSqrt(Std_absolute/(double)(total-1));
      if(n>=2)
         Std_growth=MathSqrt(Std_growth/(double)(n-1));

      // Berechnen von VaR
      out.absolute.VaR.Mx=Mx_absolute;
      out.absolute.VaR.Std=Std_absolute;
      out.absolute.VaR.VAR_90=VaR(Q_90,Mx_absolute,Std_absolute);
      out.absolute.VaR.VAR_95=VaR(Q_95,Mx_absolute,Std_absolute);
      out.absolute.VaR.VAR_99=VaR(Q_99,Mx_absolute,Std_absolute);
      out.growth.VaR.Mx=Mx_growth;
      out.growth.VaR.Std=Std_growth;
      out.growth.VaR.VAR_90=VaR(Q_90,Mx_growth,Std_growth);
      out.growth.VaR.VAR_95=VaR(Q_95,Mx_growth,Std_growth);
      out.growth.VaR.VAR_99=VaR(Q_99,Mx_growth,Std_growth);

      // Berechnen der Verteilung
      for(int i=0; i<total; i++)
        {
         Chart_item  item_a,item_g;
         ZeroMemory(item_a);
         ZeroMemory(item_g);
         item_a.x=data[i].Profit;
         item_a.y=PDF_calc(Mx_absolute,Std_absolute,data[i].Profit);
         if(i>0)
           {
            item_g.x=(data[i-1].Profit != 0 ?(data[i].Profit-data[i-1].Profit)/data[i-1].Profit : 0);
            item_g.y=PDF_calc(Mx_growth,Std_growth,item_g.x);
           }
         int s=ArraySize(out.absolute.distribution);
         ArrayResize(out.absolute.distribution,s+1,s+1);
         out.absolute.distribution[s]=item_a;
         s=ArraySize(out.growth.distribution);
         ArrayResize(out.growth.distribution,s+1,s+1);
         out.growth.distribution[s]=item_g;
        }
      // Ascending
      sorter.Sort<Chart_item>(out.absolute.distribution,&chartComparer);
      sorter.Sort<Chart_item>(out.growth.distribution,&chartComparer);
     }
  }
//+------------------------------------------------------------------+
//| Calculate VaR                                                    |
//+------------------------------------------------------------------+
double CReportCreator::VaR(double quantile,double Mx,double Std)
  {
   return Mx-quantile*Std;
  }
//+------------------------------------------------------------------+
//| Distribution calculation                                         |
//+------------------------------------------------------------------+
double CReportCreator::PDF_calc(double Mx,double Std,double x)
  {
   if(Std!=0)
      return MathExp(-0.5*MathPow((x-Mx)/Std,2))/(MathSqrt(2*M_PI)*Std);
   else
      return 0;
  }

VaR-Berechnungsmethode ist die einfachste. Sie verwendet bei den Berechnungen das historische VaR-Modell.

Die Berechnungsmethode der normalisierten Verteilung ist diejenige, die im statistischen Analysepaket von Matlab verfügbar ist.

Die Methode zur Berechnung der normalisierten Verteilung und zur Erstellung von Graphen ist eine aggregierende Methode, bei der die oben beschriebenen Methoden angewendet werden. Im ersten Zyklus wird der durchschnittliche Gewinnwert berechnet. Im Sekundenzyklus wird die Standardabweichung der Erträge berechnet. Die Renditen für das Diagramm und den VaR, die durch Wachstum berechnet werden, werden ebenfalls berechnet als normalisierte Zeitreihe. Außerdem wird nach dem Füllen des VaR-Wertes Normalverteilungsdiagramm mit der obigen Methode berechnet. Als x-Achse verwenden wir die Rentabilität für das wachstumsbasierte Diagramm und die absoluten Gewinnwerte für das gewinnbasierte Diagramm.

Zur Berechnung des Z-Scores habe ich eine Formel aus einem der Artikel dieser Website verwendet. Ihre vollständige Umsetzung ist in den angehängten Dateien verfügbar. 

Bitte beachten Sie, dass alle Berechnungen mit der Berechnungsmethode Calculate mit der folgenden Aufrufsignatur beginnen:

void CReportCreator::Create(DealDetales &history[],DealDetales &BH_history[],const double _balance,const string &Symb[],double _r);

Seine Umsetzung wurde in dem bereits erwähnten Artikel "Die 100 besten Durchläufe der Optimierung" beschrieben. Alle öffentlichen Methoden führen keine logischen Operationen durch, aber sie dienen als Getter, die die angeforderten Daten in Übereinstimmung mit den Eingabeparametern bilden und die Art der erforderlichen Informationen angeben.  

Schlussfolgerung

Im vorherigen Artikel haben wir den Prozess der Bibliotheksentwicklung in der Sprache C# betrachtet. In diesem Artikel sind wir zum nächsten Schritt übergegangen — der Erstellung eines Handelsberichts, den wir mit den erstellten Methoden erhalten können. Der Mechanismus der Berichtserstellung wurde bereits in früheren Artikeln besprochen. Er wurde jedoch verbessert und überarbeitet. Dieser Artikel stellt die neuesten Versionen dieser Entwicklungen vor. Die angebotene Lösung wurde an verschiedenen Optimierungen und Testverfahren getestet.

 

Zwei Ordner sind im angehängten Archiv verfügbar. Entpacken Sie beide in das Verzeichnis MQL/Include. 

Die folgenden Dateien sind im Anhang enthalten:

  1. CustomGeneric
    • GenericSorter.mqh
    • ICustomComparer.mqh
  2. History manager
    • CustomComissionManager.mqh
    • DealHistoryGetter.mqh
    • ReportCreator.mqh

Übersetzt aus dem Russischen von MetaQuotes Software Corp.
Originalartikel: https://www.mql5.com/ru/articles/7452

Beigefügte Dateien |
Include.zip (23.27 KB)
Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil XXVI): Arbeiten mit schwebenden Handelsanfragen - erste Implementation (Positionseröffnung) Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil XXVI): Arbeiten mit schwebenden Handelsanfragen - erste Implementation (Positionseröffnung)

In diesem Artikel werden wir einige Daten im Wert der Magicnummer der Aufträge und Positionen speichern und mit der Umsetzung der schwebenden Anträge beginnen. Um das Konzept zu überprüfen, erstellen wir die erste Testanforderung zur Eröffnung von Marktpositionen, wenn ein Serverfehler auftritt, der ein Warten und das Senden einer wiederholten Anfrage erfordert.

Mit Boxplot saisonale Muster von Finanzzeitreihen erforschen Mit Boxplot saisonale Muster von Finanzzeitreihen erforschen

In diesem Artikel werden wir die saisonalen Charakteristika von Finanzzeitreihen mit Hilfe von Boxplot-Diagrammen betrachten. Jedes separate Boxplot (oder Box-and-Whiskey-Diagramm) bietet eine gute Visualisierung der Verteilung von Werten entlang des Datensatzes. Boxplots sollten nicht mit den Kerzencharts verwechselt werden, obwohl sie visuell ähnlich aussehen.

SQLite: Natives Arbeiten mit SQL-Datenbanken in MQL5 SQLite: Natives Arbeiten mit SQL-Datenbanken in MQL5

Die Entwicklung von Handelsstrategien ist mit dem Umgang mit großen Datenmengen verbunden. Jetzt können Sie mit Datenbanken mit SQL-Abfragen auf der Basis von SQLite direkt in MQL5 arbeiten. Ein wichtiges Merkmal dieser Engine ist, dass die gesamte Datenbank in einer einzigen Datei auf dem PC des Benutzers abgelegt wird.

Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil XXVII): Arbeiten mit Handelsanfragen - platzieren von Pending-Orders Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil XXVII): Arbeiten mit Handelsanfragen - platzieren von Pending-Orders

In diesem Artikel werden wir die Entwicklung von Handelsanfragen fortsetzen, die Platzierung von Pending-Orders umsetzen und festgestellte Mängel bei der Arbeit Handelsklassen beseitigen.