Wie man Trades des ausgewählten Signals im Chart analysiert

10 August 2018, 11:16
Dmitriy Gizlyk
0
278

Inhalt

Einleitung

Ständig erscheinen neue kostenpflichtige und kostenfreie Signale im Signale-Service. Das MetaTrader-Team hat sich darum gekümmert, dass man den Service nutzen kann, ohne das Terminal zu verlassen. Man muss nur noch das Signal auswählen, das den höchsten Gewinn bei zulässigen Risiken erzielt. Diese Frage wird schon seit langem diskutiert. Es wurde bereits die Methode einer automatischen Auswahl von Signalen nach angegebenen Kriterien vorgeschlagen [1]. Aber wie der Volksmund besagt, ein Bild sagt mehr als tausend Worte. Im Artikel schlage ich vor, die Handelshistorie des ausgewählten Signals auf dem Chart eines Finanzinstruments zu studieren und zu analysieren. Wahrscheinlich hilft diese Herangehensweise, die Strategie der Ausführung von Trades besser zu verstehen und Risiken einzuschätzen. 

1. Formulieren der Ziele der bevorstehenden Arbeit

Ich höre Sie fragen: wozu das Rad neu erfinden, wenn es im Terminal bereits die Möglichkeit gibt, die Handelshistorie im Chart anzuzeigen? Es ist ausreichend, das gewünschte Signal auszuwählen und auf den Button im Terminal zu klicken.

Der Befehl "Trades im Chart anzeigen"

Im Terminal öffnen sich neue Fenster, entsprechend der Anzahl der vom Signal verwendeten Finanzinstrumenten, mit den Labels der ausgeführten Trades. Natürlich ist es sehr aufwendig, zwischen den Charts zu wechseln und nach Trades zu suchen. Darüber hinaus können Trades auf verschiedenen Charts nach Zeit zusammenfallen, und bei der Analyse jedes einzelnen Charts sieht man das nicht. Wir versuchen, einen Teil der Arbeit gerade in dieser Phase zu automatisieren.

Um festzustellen, welches Werkzeug wir für die Analyse der erhaltenen Charts brauchen, müssen wir eine klare Vorstellung haben, welche Endergebnisse wir brauchen. Hier sind die Schwerpunkte:

  • schauen, wie das Signal auf verschiedenen Finanzinstrumenten arbeitet;
  • wie Deposit Load verteilt wird, wie viele Positionen gleichzeitig offen sein können;
  • wenn gleichzeitig mehrere Positionen durch das Signal eröffnet werden, ob das gehedgte Positionen sind oder ob sie die Belastung auf das Konto erhöhen;
  • wann und auf welchen Finanzinstrumenten der höchste Rückgang zu verzeichnen ist;
  • wann der höchste Gewinn erzielt wird.

2. Sammeln der Statistik von Trades

2.1. Die Klasse für das Speichern von Informationen über eine Order

Wählen wir das gewünschte Signal aus und geben wir seine Handelshistorie im Chart aus. Sammeln wir die Ausgangsdaten, die wir später analysieren werden. Um die Daten zu jeder einzelnen Order zu schreiben, erstellen wir die Klasse COrder basierend auf der Klasse CObject. In den Variablen dieser Klasse speichern wir das Ticket der Order, das Volumen und den Typ des Trades, Preis des Trades, Typ der Transkation (Einstieg/Ausstieg), Zeit der Eröffnung der Order und natürlich das Symbol.

class COrder : public CObject
  {
private:
   long                 l_Ticket;
   double               d_Lot;
   double               d_Price;
   ENUM_POSITION_TYPE   e_Type;
   ENUM_DEAL_ENTRY      e_Entry;
   datetime             dt_OrderTime;
   string               s_Symbol;
   
public:
                        COrder();
                       ~COrder();
   bool                 Create(string symbol, long ticket, double volume, double price, datetime time, ENUM_POSITION_TYPE type);
//---
   string               Symbol(void)   const {  return s_Symbol;     }
   long                 Ticket(void)   const {  return l_Ticket;     }
   double               Volume(void)   const {  return d_Lot;        }
   double               Price(void)    const {  return d_Price;      }
   datetime             Time(void)     const {  return dt_OrderTime; } 
   ENUM_POSITION_TYPE   Type(void)           {  return e_Type;       }
   ENUM_DEAL_ENTRY      DealEntry(void)const {  return e_Entry;      }
   void                 DealEntry(ENUM_DEAL_ENTRY value) {  e_Entry=value; }
//--- methods for working with files
   virtual bool         Save(const int file_handle);
   virtual bool         Load(const int file_handle);
//---
   //--- method of comparing the objects
   virtual int          Compare(const CObject *node,const int mode=0) const;
  };

Neben der Funktion, mit der auf Daten zugegriffen wird, fügen wir der Klasse der Orders die Funktionen für die Arbeit mit Dateien hinzu, um die Daten zu speichern und anschließend zu lesen, sowie die Funktion des Vergleichs mit einem ähnlichen Objekt, die für das Sortieren von Orders benötigt wird.

Um zwei Orders zu vergleichen, müssen wir die virtuelle Funktion Compare umschreiben. Das ist eine Funktion der Basisklasse, die zum Vergleich von zwei CObject Objekten dient. Deswegen wird in ihren Parametern ein Verweis zum CObject Objekt und zur Sortierungsmethode übergeben. Die eigenen Orders werden wir nur in einer Richtung sortieren (nach Ausführungsdatum aufsteigend), deswegen wird der mode Parameter im Code der Funktion nicht verwendet. Für die Arbeit mit dem COrder Objekt, der nach dem Verweis erhalten wurde, muss sein Typ umgewandelt werden. Danach vergleichen wir die Daten der erhaltenen und der aktuellen Order. Wenn die erhaltene Order älter ist — "-1" ausgeben, wenn aktueller — "1". Wenn die Ausführungsdaten der Orders übereinstimmen, gibt die Funktion 0 zurück.

int COrder::Compare(const CObject *node,const int mode=0) const
  {
   const COrder *temp=GetPointer(node);
   if(temp.Time()>dt_OrderTime)
      return -1;
//---
   if(temp.Time()<dt_OrderTime)
      return 1;
//---
   return 0;
  }

2.2. Sammeln von Informationen von Charts

Für die Arbeit mit Orders erstellen wir die Klasse COrdersCollection basierend auf der Klasse CArrayObj. In dieser Klasse werden Informationen gesammelt und verarbeitet. Für das Speichern von Daten deklarieren wir eine Instanz des Objekts für die Arbeit mit einer bestimmten Order und ein Array für das Speichern der verwendeten Finanzinstrumenten. Das Array der Orders wird mithilfe der Funktion der Basisklasse gespeichert.

class COrdersCollection : public CArrayObj
  {
private:
   COrder            *Temp;
   string            ar_Symbols[];
   
public:

                     COrdersCollection();
                    ~COrdersCollection();
//--- Initialisierung
   bool              Create(void);
//--- Hinzufügen einer Order
   bool              Add(COrder *element);
//--- Zugriff auf Daten
   int               Symbols(string &array[]);
   bool              GetPosition(const string symbol, const datetime time, double &volume, double &price, ENUM_POSITION_TYPE &type);
   datetime          FirstOrder(const string symbol=NULL);
   datetime          LastOrder(const string symbol=NULL);
//--- Erhalten der Zeitreihen
   bool              GetTimeSeries(const string symbol, const datetime start_time, const datetime end_time, const int direct,
                                   double &balance[], double &equity[], double &time[], double &profit, double &loss,int &long_trades, int &short_trades);
//---
   void              SetDealsEntry(void);
  };

Für das Sammeln von Daten ist die Funktion Create zuständig. Im Body der Methode erstellen wir eine Schleife für das Iterieren über alle offenen Charts im Terminal. In jedem Chart suchen wir nach den grafischen Objekten vom Typ OBJ_ARROW_BUY und OBJ_ARROW_SELL.

bool COrdersCollection::Create(void)
  {
   long chart=ChartFirst();
   while(chart>0)
     {
      int total_buy=ObjectsTotal(chart,0,OBJ_ARROW_BUY);
      int total_sell=ObjectsTotal(chart,0,OBJ_ARROW_SELL);
      if((total_buy+total_sell)<=0)
        {
         chart=ChartNext(chart);
         continue;
        }

Wenn sich das Objekt auf dem Chart befindet, fügen wir dem Array der Finanzinstrumente das Symbol des Charts hinzu (aber zuerst überprüfen wir, ob es dieses Symbol nicht unter den bereits gespeicherten gibt).

      int symb=ArraySize(ar_Symbols);
      string symbol=ChartSymbol(chart);
      bool found=false;
      for(int i=0;(i<symb && !found);i++)
         if(ar_Symbols[i]==symbol)
           {
            found=true;
            symb=i;
            break;
           }
      if(!found)
        {
         if(ArrayResize(ar_Symbols,symb+1,10)<=0)
            return false;
         ar_Symbols[symb]=symbol;
        }

Danach sammeln wird die Informationen über Trades in einen Datenarray. Bitte beachten Sie: die einzige Informationsquelle über einen Trade ist das grafische Objekt. Von den Objektparametern können wir nur die Zeit und den Preis des Trades abfragen. Alle anderen Informationen entnehmen wir dem Objektnamen, der eine Textzeile darstellt.

Name des grafischen Objekts

Auf dem Bild sieht man, dass der Objektname alle Informationen über den Trade (getrennt durch Leerstellen) enthält. Nutzen wir das und teilen die Zeile nach Leerstellen in einen Array von String-Elementen. Danach wandeln wir die Daten des entsprechenden Elements in den benötigten Typ um und speichern sie. Nach dem Sammeln der Informationen wechseln wir zum nächsten Chart.

      int total=fmax(total_buy,total_sell);
      for(int i=0;i<total;i++)
        {
         if(i<total_buy)
           {
            string name=ObjectName(chart,i,0,OBJ_ARROW_BUY);
            datetime time=(datetime)ObjectGetInteger(chart,name,OBJPROP_TIME);
            StringTrimLeft(name);
            StringTrimRight(name);
            StringReplace(name,"#","");
            string split[];
            StringSplit(name,' ',split);
            Temp=new COrder;
            if(CheckPointer(Temp)!=POINTER_INVALID)
              {
               if(Temp.Create(ar_Symbols[symb],StringToInteger(split[1]),StringToDouble(split[3]),StringToDouble(split[6]),time,POSITION_TYPE_BUY))
                  Add(Temp);
              }
           }
//---
         if(i<total_sell)
           {
            string name=ObjectName(chart,i,0,OBJ_ARROW_SELL);
            datetime time=(datetime)ObjectGetInteger(chart,name,OBJPROP_TIME);
            StringTrimLeft(name);
            StringTrimRight(name);
            StringReplace(name,"#","");
            string split[];
            StringSplit(name,' ',split);
            Temp=new COrder;
            if(CheckPointer(Temp)!=POINTER_INVALID)
              {
               if(Temp.Create(ar_Symbols[symb],StringToInteger(split[1]),StringToDouble(split[3]),StringToDouble(split[6]),time,POSITION_TYPE_SELL))
                  Add(Temp);
              }
           }
        }
      chart=ChartNext(chart);
     }

Die grafischen Labels beinhalten keine Informationen darüber, ob eine Position für jeden Trade eröffnet oder geschlossen wurde. Aus diesem Grund war dieses Feld beim Speichern von Informationen über Trades leer. Nachdem alle Labels vom Chart gesammelt wurden, fügen wir fehlende Informationen hinzu, indem wir die Funktion SetDealsEntry aufrufen.

   SetDealsEntry();
//---
   return true;
  }

Um doppelte Trades in der Datenbank zu vermeiden, schreiben wir die Funktion Add um: fügen wir ihr die Prüfung des Vorhandenseins einer Order nach Ticket hinzu.

bool COrdersCollection::Add(COrder *element)
  {
   for(int i=0;i<m_data_total;i++)
     {
      Temp=m_data[i];
      if(Temp.Ticket()==element.Ticket())
         return true;
     }
//---
   return CArrayObj::Add(element);
  }

Um die Typen von Operationen in den Trades anzuordnen, erstellen wir die Funktion SetDealsEntry. Am Anfang der Funktion rufen wir die Funktion der Sortierung der Basisklasse auf. Danach erstellen wir eine Schleife für das Iterieren über alle Symbole und Trades auf jedem Symbol. Der Algorithmus zum Bestimmen des Typs der Transaktion ist einfach. Wenn es zum Zeitpunkt der Transaktion keine offene Position gibt, oder wenn es eine Position in der Richtung des Trades gibt, definieren wir die Transaktion als Einstieg. Wenn die Transaktion der existierenden Position entgegengesetzt ist, wird ihr Volumen für das Schließen der offenen Position verwendet, der Rest eröffnet eine neue Position (ähnlich zum Netting-System im MetaTrader 5).

COrdersCollection::SetDealsEntry(void)
  {
   Sort(0);
//---
   int symbols=ArraySize(ar_Symbols);
   for(int symb=0;symb<symbols;symb++)
     {
      double volume=0;
      ENUM_POSITION_TYPE type=-1;
      for(int ord=0;ord<m_data_total;ord++)
        {
         Temp=m_data[ord];
         if(Temp.Symbol()!=ar_Symbols[symb])
            continue;
//---
         if(volume==0 || type==Temp.Type())
           {
            Temp.DealEntry(DEAL_ENTRY_IN);
            volume=NormalizeDouble(volume+Temp.Volume(),2);
            type=Temp.Type();
           }
         else
           {
            if(volume>=Temp.Volume())
              {
               Temp.DealEntry(DEAL_ENTRY_OUT);
               volume=NormalizeDouble(volume-Temp.Volume(),2);
              }
            else
              {
               Temp.DealEntry(DEAL_ENTRY_INOUT);
               volume=NormalizeDouble(volume-Temp.Volume(),2);
               type=Temp.Type();
              }
           }
        }
     }
  }

2.3. Erstellen der Zeitreihen von Kontostand und Equity für jedes Finanzinstrument

Um später die Grafiken des Kontostands und der Equity für jedes Symbol zu zeichnen, müssen wir Zeitreihen mit der Berechnung dieser Parameter innerhalb der zu analysierenden Periode erstellen. Bei der Analyse sollten wir die Möglichkeit haben, den zu analysierenden Zeitraum zu ändern. Dies erlaubt es, die Arbeit des Signals innerhalb von begrenzten Intervallen zu studieren.

Die Zeitreihen werden in der Funktion GetTimeSeries berechnet. In ihren Parametern geben wir das Symbol, das Anfangsdatum und Enddatum der zu analysierenden Periode sowie die Richtung des Handels an, um Long- und Short-Positionen zu verfolgen. Die Funktion wird drei Zeitreihen zurückgeben: Kontostand, Equity und Zeit-Labels. Darüber hinaus gibt sie Statistiken über das Symbol für den zu analysierenden Zeitraum zurück: Gewinn, Verlust, Long- und Short-Trades.

Ich möchte Sie darauf hinweisen, dass das Array für die Zeitreihe von Zeit-Labels als double definiert wurde. Dieser kleine Trick ist eine Notlösung. Weiter werden wir die Grafiken des Kontostands und der Equity unter Verwendung der Standardklasse CGraphic zeichnen, die nur Arrays vom Typ double akzeptiert.

Am Anfang der Funktion setzen wir die Variablen für das Sammeln der Statistik auf Null, überprüfen die Richtigkeit des angegebenen Symbols und erhalten den Preis eines Punktes der Preisänderung.

bool COrdersCollection::GetTimeSeries(const string symbol,const datetime start_time,const datetime end_time,const int direct,double &balance[],double &equity[], double &time[], double &profit, double &loss,int &long_trades, int &short_trades)
  {
   profit=loss=0;
   long_trades=short_trades=0;
//---
   if(symbol==NULL)
      return false;
//---
   double tick_value=SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE)/SymbolInfoDouble(symbol,SYMBOL_POINT);
   if(tick_value==0)
      return false;

Um Zeitreihen zu erstellen, verwenden wir die Kurse des Symbols vom Zeitrahmen M5, sie müssen heruntergeladen werden. Aber bitte beachten Sie, die angeforderten Kurse können noch nicht vollständig sein. Ein weiterer Trick: wir werden die Operationen nicht in einer Schleife ausführen und warten, bis die Daten komplett heruntergeladen werden, denn das würde die Ausführung des Programms stoppen und bei der Verwendung in Indikatoren die Arbeit des Terminals verlangsamen. Nach dem ersten fehlgeschlagenen Zugriff verlassen wir die Funktion, aber davor erstellen wir ein benutzerdefiniertes Ereignis, das später die Funktion der Datenaktualisierung aufruft.

   ENUM_TIMEFRAMES timeframe=PERIOD_M5;
//---
   double volume=0;
   double price=0;
   ENUM_POSITION_TYPE type=-1;
   int order=-1;
//---
   MqlRates rates[];
   int count=0;
   count=CopyRates(symbol,timeframe,start_time,end_time,rates);
   if(count<=0 && !ReloadHistory)
     {
      //--- send notification
      ReloadHistory=EventChartCustom(CONTROLS_SELF_MESSAGE,1222,0,0.0,symbol);
      return false;
     }

Nach dem Herunterladen der Kurse passen wir die Größe der Arrays der Zeitreihen der Größe der heruntergeladenen Kurse an.

   if(ArrayResize(balance,count)<count || ArrayResize(equity,count)<count || ArrayResize(time,count)<count)
      return false;
   ArrayInitialize(balance,0);

Danach erstellen wir eine Schleife, um die Informationen für Zeitreihen zu sammeln. Auf jedem Balken bestimmen wir die ausgeführten Operationen. Wenn es sich um die Eröffnung einer Position handelt, erhöhen wir das Volumen der aktuellen Position und berechnen den durchschnittlichen Open-Preis neu. Wenn es um das Schließen einer Position geht, berechnen wir den Gewinn/Verlust der Operation, den erhaltenen Wert addieren wir zum Wert der Änderung des Kontostandes auf dem aktuellen Balken und reduzieren das Volumen der aktuellen Position. Für die Lotgröße der Position, die zum Zeitpunkt des Schließens des Balkens nicht geschlossen wurde, berechnen wir den noch offenen Gewinn/Verlust und speichern den erhaltenen Wert in der Änderung der Equity auf dem zu analysierenden Balken. Nachdem wir über die komplette Historie iteriert haben, verlassen wir die Funktion.

   do
     {
      order++;
      if(order<m_data_total)
         Temp=m_data[order];
      else
         Temp=NULL;
     }
   while(CheckPointer(Temp)==POINTER_INVALID && order<m_data_total);
//---
   for(int i=0;i<count;i++)
     {
      while(order<m_data_total && Temp.Time()<(rates[i].time+PeriodSeconds(timeframe)))
        {
         if(Temp.Symbol()!=symbol)
           {
            do
              {
               order++;
               if(order<m_data_total)
                  Temp=m_data[order];
               else
                  Temp=NULL;
              }
            while(CheckPointer(Temp)==POINTER_INVALID && order<m_data_total);
            continue;
           }
//---
         if(Temp!=NULL)
           {
            if(type==Temp.Type())
              {
               price=volume*price+Temp.Volume()*Temp.Price();
               volume+=Temp.Volume();
               price=price/volume;
               switch(type)
                 {
                  case POSITION_TYPE_BUY:
                    long_trades++;
                    break;
                  case POSITION_TYPE_SELL:
                    short_trades++;
                    break;
                 }
              } 
            else
              {
               if(i>0 && (direct<0 || direct==type))
                 {
                  double temp=(Temp.Price()-price)*tick_value*(type==POSITION_TYPE_BUY ? 1 : -1)*MathMin(volume,Temp.Volume());
                  balance[i]+=temp;
                  if(temp>=0)
                     profit+=temp;
                  else
                     loss+=temp;
                 }
               volume-=Temp.Volume();
               if(volume<0)
                 {
                  volume=MathAbs(volume);
                  price=Temp.Price();
                  type=Temp.Type();
                  switch(type)
                    {
                     case POSITION_TYPE_BUY:
                       long_trades++;
                       break;
                     case POSITION_TYPE_SELL:
                       short_trades++;
                       break;
                    }
                 }
              }
           }
         do
           {
            order++;
            if(order<m_data_total)
               Temp=m_data[order];
            else
               Temp=NULL;
           }
         while(CheckPointer(Temp)==POINTER_INVALID && order<m_data_total);
        }
      if(i>0)
        {
         balance[i]+=balance[i-1];
        }
      if(volume>0 && (direct<0 || direct==type))
         equity[i]=(rates[i].close-price)*tick_value*(type==POSITION_TYPE_BUY ? 1 : -1)*MathMin(volume,(Temp!=NULL ? Temp.Volume(): DBL_MAX));
      else
         equity[i]=0;
      equity[i]+=balance[i];
      time[i]=(double)rates[i].time;
     }
//---
   return true;
  }

Der vollständige Code der Klassen und Methoden kann in den beigefügten Dateien studiert werden.
 

3. Hinzufügen einer grafischen Oberfläche

Das grafische Interface des Programms beinhaltet das Anfangs- und Enddatum der Analyse, Checkboxen für die Auswahl der im Chart angezeigten Informationen, einen Statistik-Block und die Charts selbst.

Grafisches Interface

Das grafische Interface wird in der Klasse CStatisticsPanel erstellt (die Klasse wurde von der Klasse CAppDialog abgeleitet). Für die Auswahl des Anfangs- und Enddatums der Analyse verwenden wir die Instanzen der Klasse CDatePicker. Die Checkboxen für die Auswahl der anzuzeigenden Informationen vereinen wir in drei Gruppen:

  • Kontostand und Equity;
  • Long- und Short-Positionen;
  • Liste der zu analysierenden Instrumente.

3.1. Erstellen eines grafischen Panels

Für die Erstellung der Checkboxen verwenden wir Instanzen der Klasse CCheckGroup. Die Textstatistik geben wir mithilfe der Instanzen der Klasse CLabel aus. Die Charts zeichnen wir mithilfe einer Instanz der Klasse CGraphic. Für den Zugriff auf unsere Statistik der Orders deklarieren wir eine Instanz der Klasse COrdersCollection.

class CStatisticsPanel : public CAppDialog
  {
private:
   CDatePicker       StartDate;
   CDatePicker       EndDate;
   CLabel            Date;
   CGraphic          Graphic;
   CLabel            ShowLabel;
   CCheckGroup       Symbols;
   CCheckGroup       BalEquit;
   CCheckGroup       Deals;
   string            ar_Symbols[];
   CLabel            TotalProfit;
   CLabel            TotalProfitVal;
   CLabel            GrossProfit;
   CLabel            GrossProfitVal;
   CLabel            GrossLoss;
   CLabel            GrossLossVal;
   CLabel            TotalTrades;
   CLabel            TotalTradesVal;
   CLabel            LongTrades;
   CLabel            LongTradesVal;
   CLabel            ShortTrades;
   CLabel            ShortTradesVal;
   //---
   COrdersCollection Orders;

public:
                     CStatisticsPanel();
                    ~CStatisticsPanel();
   //--- main application dialog creation and destroy
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);
   virtual void      Destroy(const int reason=REASON_PROGRAM);
   //--- chart event handler
   virtual bool      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);

protected:
   virtual bool      CreateLineSelector(const string name,const int x1,const int y1,const int x2,const int y2);
   virtual bool      CreateDealsSelector(const string name,const int x1,const int y1,const int x2,const int y2);
   virtual bool      CreateCheckGroup(const string name,const int x1,const int y1,const int x2,const int y2);
   virtual bool      CreateGraphic(const string name,const int x1,const int y1,const int x2,const int y2);
   //---
   virtual void      Maximize(void);
   virtual void      Minimize(void);
   //---
   virtual bool      UpdateChart(void);

  };

In der Methode Create rufen wir zuerst die entsprechende Methode der Elternklasse auf und dann ordnen wir alle Objekte an und initialisieren die Instanz der Klasse der Sammlung von Orders. Nach der Initialisierung jedes Elements vergessen Sie nicht, initiale Werte zuzuweisen und das Objekt zur Sammlung der Steuerelemente hinzuzufügen. Die Arbeit mit der Basisklasse wurde in den Artikeln [2] und [3] beschrieben, deswegen werde ich darauf nicht eingehen, ich führe nur den Code der Methode an.

bool CStatisticsPanel::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2)
  {
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      return false;
//---
   if(!TotalProfit.Create(m_chart_id,m_name+"Total Profit",m_subwin,5,80,115,95))
      return false;
   if(!TotalProfit.Text("Total Profit"))
      return false;
   if(!Add(TotalProfit))
      return false;
//---
   if(!TotalProfitVal.Create(m_chart_id,m_name+"Total Profit Value",m_subwin,135,80,250,95))
      return false;
   if(!TotalProfitVal.Text("0"))
      return false;
   if(!Add(TotalProfitVal))
      return false;
//---
   if(!GrossProfit.Create(m_chart_id,m_name+"Gross Profit",m_subwin,5,100,115,115))
      return false;
   if(!GrossProfit.Text("Gross Profit"))
      return false;
   if(!Add(GrossProfit))
      return false;
//---
   if(!GrossProfitVal.Create(m_chart_id,m_name+"Gross Profit Value",m_subwin,135,100,250,115))
      return false;
   if(!GrossProfitVal.Text("0"))
      return false;
   if(!Add(GrossProfitVal))
      return false;
//---
   if(!GrossLoss.Create(m_chart_id,m_name+"Gross Loss",m_subwin,5,120,115,135))
      return false;
   if(!GrossLoss.Text("Gross Loss"))
      return false;
   if(!Add(GrossLoss))
      return false;
//---
   if(!GrossLossVal.Create(m_chart_id,m_name+"Gross Loss Value",m_subwin,135,120,250,135))
      return false;
   if(!GrossLossVal.Text("0"))
      return false;
   if(!Add(GrossLossVal))
      return false;
//---
   if(!TotalTrades.Create(m_chart_id,m_name+"Total Trades",m_subwin,5,150,115,165))
      return false;
   if(!TotalTrades.Text("Total Trades"))
      return false;
   if(!Add(TotalTrades))
      return false;
//---
   if(!TotalTradesVal.Create(m_chart_id,m_name+"Total Trades Value",m_subwin,135,150,250,165))
      return false;
   if(!TotalTradesVal.Text("0"))
      return false;
   if(!Add(TotalTradesVal))
      return false;
//---
   if(!LongTrades.Create(m_chart_id,m_name+"Long Trades",m_subwin,5,170,115,185))
      return false;
   if(!LongTrades.Text("Long Trades"))
      return false;
   if(!Add(LongTrades))
      return false;
//---
   if(!LongTradesVal.Create(m_chart_id,m_name+"Long Trades Value",m_subwin,135,170,250,185))
      return false;
   if(!LongTradesVal.Text("0"))
      return false;
   if(!Add(LongTradesVal))
      return false;
//---
   if(!ShortTrades.Create(m_chart_id,m_name+"Short Trades",m_subwin,5,190,115,215))
      return false;
   if(!ShortTrades.Text("Short Trades"))
      return false;
   if(!Add(ShortTrades))
      return false;
//---
   if(!ShortTradesVal.Create(m_chart_id,m_name+"Short Trades Value",m_subwin,135,190,250,215))
      return false;
   if(!ShortTradesVal.Text("0"))
      return false;
   if(!Add(ShortTradesVal))
      return false;
//---
   if(!Orders.Create())
      return false;
//---
   if(!ShowLabel.Create(m_chart_id,m_name+"Show Selector",m_subwin,285,8,360,28))
      return false;
   if(!ShowLabel.Text("Symbols"))
      return false;
   if(!Add(ShowLabel))
      return false;
   if(!CreateLineSelector("LineSelector",2,30,115,70))
      return false;
   if(!CreateDealsSelector("DealsSelector",135,30,250,70))
      return false;
   if(!CreateCheckGroup("CheckGroup",260,30,360,ClientAreaHeight()-5))
      return false;
//---
   if(!Date.Create(m_chart_id,m_name+"->",m_subwin,118,8,133,28))
      return false;
   if(!Date.Text("->"))
      return false;
   if(!Add(Date))
      return false;
//---
   if(!StartDate.Create(m_chart_id,m_name+"StartDate",m_subwin,5,5,115,28))
      return false;
   if(!Add(StartDate))
      return false;
//---
   if(!EndDate.Create(m_chart_id,m_name+"EndDate",m_subwin,135,5,250,28))
      return false;
   if(!Add(EndDate))
      return false;
//---
   StartDate.Value(Orders.FirstOrder());
   EndDate.Value(Orders.LastOrder());
//---
   if(!CreateGraphic("Chraphic",370,5,ClientAreaWidth()-5,ClientAreaHeight()-5))
      return false;
//---
   UpdateChart();
//---
   return true;
  }

Einem aufmerksamen Leser kann auffallen, dass der erstellte Chart nicht zur Sammlung der Steuerelemente hinzugefügt wurde. Es ist damit verbunden, dass das Objekt CGraphic nicht von der Klasse CWnd abgeleitet wurde, und zur Sammlung nur die von der CWnd Klasse abgeleiteten Objekte hinzugefügt werden können. Deswegen müssen wir die Funktionen der Minimierung und Maximierung des Panels umschreiben.
Nach der Initialisierung aller Objekte rufen wir die Funktion der Aktualisierung des Charts auf.

3.2. Funktion der Erstellung des Charts

Gehen wir kurz auf CreateGraphic, die Funktion der Erstellung des Charts, ein. In den Parametern erhält sie den Namen des zu erstellenden Objekts und die Koordinaten des Charts. Am Anfang der Funktion wird ein Chart erstellt (Aufruf der Funktion Create der Klasse CGraphic). Da die Klasse CGraphic nicht von der Klasse CWnd abgeleitet wird und wir sie der Sammlung der Steuerelemente des Panels nicht hinzufügen können, verschieben wir die Koordinaten des Charts entsprechend der Position des Kundenbereichs.

bool CStatisticsPanel::CreateGraphic(const string name,const int x1,const int y1,const int x2,const int y2)
  {
   if(!Graphic.Create(m_chart_id,m_name+name,m_subwin,ClientAreaLeft()+x1,ClientAreaTop()+y1,ClientAreaLeft()+x2,ClientAreaTop()+y2))
      return false;

Danach müssen wir die Instanzen der Klasse CCurve für jede Kurve erstellen, die auf dem Chart angezeigt wird. Dafür fragen wir zuerst die Liste der verwendeten Finanzinstrumente aus der Instanz der Klasse COrdersCollection ab. Danach erstellen wir die Kurven des Kontostands und der Equity in der Schleife für jedes Instrument, indem wir sie mit einem leeren Array initialisieren. Nach der Erstellung blenden wir die Linien im Chart aus, bis die Daten erhalten werden.

   int total=Orders.Symbols(ar_Symbols);
   CColorGenerator ColorGenerator;
   double array[];
   ArrayFree(array);
   for(int i=0;i<total;i++)
     {
      //---
      CCurve *curve=Graphic.CurveAdd(array,array,ColorGenerator.Next(),CURVE_LINES,ar_Symbols[i]+" Balance");
      curve.Visible(false);
      curve=Graphic.CurveAdd(array,array,ColorGenerator.Next(),CURVE_LINES,ar_Symbols[i]+" Equity");
      curve.Visible(false);
     }

Nach der Erstellung der Kurven deaktivieren wir das Auto-Scaling der Abszissenskala und geben für sie die Eigenschaft an, als Daten anzuzeigen. Geben wir auch die Größe für die Anzeige der Beschriftungen der Kurven an und geben den Chart auf den Bildschirm aus.

   CAxis *axis=Graphic.XAxis();
   axis.AutoScale(false);
   axis.Type(AXIS_TYPE_DATETIME);
   axis.ValuesDateTimeMode(TIME_DATE);
   Graphic.HistorySymbolSize(20);
   Graphic.HistoryNameSize(10);
   Graphic.HistoryNameWidth(60);
   Graphic.CurvePlotAll();
   Graphic.Update();
//---
   return true;
  }

3.3. Methode der Aktualisierung des Charts und der statistischen Daten

Die Informationen über das Signal werden mithilfe der Methode UpdateChart aktualisiert. Am Anfang der Funktion bereiten wir die Variablen und Arrays für das Sammeln der Daten vor.

bool CStatisticsPanel::UpdateChart(void)
  {
   double balance[];
   double equity[];
   double time[];
   double total_profit=0, total_loss=0;
   int total_long=0, total_short=0;
   CCurve *Balance, *Equity;

Danach erhalten wir das Anfangsdatum und das Enddatum des zu analysierenden Zeitraums.

   datetime start=StartDate.Value();
   datetime end=EndDate.Value();

Überprüfen, ob die Labels die Statistik für Long- und Short-Positionen anzeigen.

   int deals=-2;
   if(Deals.Check(0))
      deals=(Deals.Check(1) ? -1 : POSITION_TYPE_BUY);
   else
      deals=(Deals.Check(1) ? POSITION_TYPE_SELL : -2);

Nach der Vorbereitung der Ausgangsdaten in der Schleife für jedes Finanzinstrument aktualisieren wir die Zeitreihen, indem wir die bereits bekannte Funktion GetTimeSeries aufrufen. Vor dem Aufruf der Methode prüfen wir das Label in der Checkbox des entsprechenden Symbols. Wenn es kein Label gibt, wird die Methode nicht aufgerufen, die Kurven werden ausgeblendet. Nach einer erfolgreichen Abfrage der Zeitreihen aktualisieren wir die Daten für die Kurven des Kontostandes und der Equity, zuerst prüfen wir aber die Labels in den entsprechenden Checkboxen. Wenn es kein Label gibt, wird die Kurve im Chart ausgeblendet.

   int total=ArraySize(ar_Symbols);
   for(int i=0;i<total;i++)
     {
      Balance  =  Graphic.CurveGetByIndex(i*2);
      Equity   =  Graphic.CurveGetByIndex(i*2+1);
      double profit,loss;
      int long_trades, short_trades;
      if(deals>-2 && Symbols.Check(i) && Orders.GetTimeSeries(ar_Symbols[i],start,end,deals,balance,equity,time,profit,loss,long_trades,short_trades))
        {
         if(BalEquit.Check(0))
           {
            Balance.Update(time,balance);
            Balance.Visible(true);
           }
         else
            Balance.Visible(false);
         if(BalEquit.Check(1))
           {
            Equity.Update(time,equity);
            Equity.Visible(true);
           }
         else
            Equity.Visible(false);
         total_profit+=profit;
         total_loss+=loss;
         total_long+=long_trades;
         total_short+=short_trades;
        }
      else
        {
         Balance.Visible(false);
         Equity.Visible(false);
        }
     }

Als Nächstes geben wir den Anfangsdatum und das Enddatum des zu analysierenden Zeitraums und die Größe des Gitters an. Aktualisieren wir den Chart. 

   CAxis *axis=Graphic.XAxis();
   axis.Min((double)start);
   axis.Max((double)end);
   axis.DefaultStep((end-start)/5);
   if(!Graphic.Redraw(true))
      return false;
   Graphic.Update();

Am Ende der Methode aktualisieren wir die Informationen in den Text-Labels für die Ausgabe der Statistik über das Signal.

   if(!TotalProfitVal.Text(DoubleToString(total_profit+total_loss,2)))
      return false;
   if(!GrossProfitVal.Text(DoubleToString(total_profit,2)))
      return false;
   if(!GrossLossVal.Text(DoubleToString(total_loss,2)))
      return false;
   if(!TotalTradesVal.Text(IntegerToString(total_long+total_short)))
      return false;
   if(!LongTradesVal.Text(IntegerToString(total_long)))
      return false;
   if(!ShortTradesVal.Text(IntegerToString(total_short)))
      return false;
//---
   return true;
  }

3.4. "Beleben" des Panels

Um das Panel zu "beleben", müssen wir einen Event Handler für die Aktionen mit Objekten erstellen. Welche Ereignisse wird das Programm verarbeiten?

In erster Linie, die Änderung des Anfangs- oder des Enddatums des zu analysierenden Zeitraums und die Änderung des Status der Checkboxen, von welchen das Sammeln der Statistik und die Ausgabe der Kontostand- und Equity-Kurven verwaltet wird. Unseren Trick nicht vergessen: wir werden ein Ereignis verarbeiten, das erstellt wurde, als es unmöglich war, die Preishistorie eines der zu analysierenden Finanzinstrumente herunterzuladen. Wenn eines dieser Ereignisse auftritt, genügt es, die Methode der Datenaktualisierung UpdateChart aufzurufen. Die Methode der Verarbeitung der Ereignisse wird wie folgt aussehen:

EVENT_MAP_BEGIN(CStatisticsPanel)
   ON_EVENT(ON_CHANGE,Symbols,UpdateChart)
   ON_EVENT(ON_CHANGE,BalEquit,UpdateChart)
   ON_EVENT(ON_CHANGE,Deals,UpdateChart)
   ON_EVENT(ON_CHANGE,StartDate,UpdateChart)
   ON_EVENT(ON_CHANGE,EndDate,UpdateChart)
   ON_NO_ID_EVENT(1222,UpdateChart)
EVENT_MAP_END(CAppDialog)

Neben den angegebenen Methoden haben wir die Methoden für die Minimierung/Maximierung des Panels geändert: ihnen wurde die Funktion zum Ausblenden/Anzeigen des Charts hinzugefügt. Der vollständige Code der Klasse und Methoden kann in den beigefügten Dateien studiert werden.

4. Erstellen eines Indikators für die Analyse des Signals

Ich schlage vor, alles oben beschriebene in Form eines Indikators umzusetzen. Dies ermöglicht es uns, das grafische Panel in einem Unterfenster zu erstellen, ohne den Chart selbst zu involvieren.

Die ganze Funktionalität des Programms befindet sich in der Klasse CStatisticsPanel. D.h. für die Erstellung eines Indikators genügt es, eine Instanz dieser Klasse in unserem Programm zu erstellen. Initialisieren wir die Klasse OnInit.

int OnInit()
  {
//---
   long chart=ChartID();
   int subwin=ChartWindowFind();
   IndicatorSetString(INDICATOR_SHORTNAME,"Signal Statistics");
   ReloadHistory=false;
//---
   Dialog=new CStatisticsPanel;
   if(CheckPointer(Dialog)==POINTER_INVALID)
     {
      ChartIndicatorDelete(chart,subwin,"Signal Statistics");
      return INIT_FAILED;
     }
   if(!Dialog.Create(chart,"Signal Statistics",subwin,0,0,0,250))
     {
      ChartIndicatorDelete(chart,subwin,"Signal Statistics");
      return INIT_FAILED;
     }
   if(!Dialog.Run())
     {
      ChartIndicatorDelete(chart,subwin,"Signal Statistics");
      return INIT_FAILED;
     }
//---
   return(INIT_SUCCEEDED);
  }

 Die Funktion OnCalculate bleibt leer, denn das Programm wird nicht auf das Eintreffen des nächsten Ticks reagieren. Es bleibt uns nur noch, den Aufruf der entsprechenden Methoden in der Funktion OnDeinit und OnChartEvent hinzuzufügen.

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   Dialog.ChartEvent(id,lparam,dparam,sparam);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   Dialog.Destroy(reason);
   delete Dialog;
  }

Nachdem der Indikator kompiliert wurde, laden wir die Statistik des ausgewählten Signals auf die Charts des Terminals hoch und fügen einem der Charts unseren Indikator hinzu. Nun kann man Trades studieren und analysieren. Es gibt eine Nuance: wir haben die Charts für die Analyse in unserem Programm nicht gefiltert. Deswegen sammelt der Indikator Statistik von allen im Terminal offenen Charts. Um die Trades des Indikators nicht mit den anderen Trades im Terminal zu vermischen, empfehle ich vor dem Download der Handelshistorie des Signals alle Charts zu schließen.

Beispiel für die Arbeit des Indikators

Den vollständigen Code des Programms können Sie im Anhang finden.

Fazit

Wir haben einen Indikator erstellt, der Trades nach Labels in Charts analysiert. Diese Technologie kann für verschiedene Zwecke verwendet werden: zum Beispiel bei der Auswahl eines Signals oder für die Optimierung einer eigenen Strategie. So können Sie Symbole feststellen, für welche unsere Strategie nicht funktioniert, um die Strategie auf diesen Symbolen in der Zukunft nicht zu verwenden.

Links

  1. Automatische Auswahl vielversprechender Signale
  2. Wie erstellt man ein grafisches Panel beliebiger Komplexität?
  3. Panels verbessern: Transparenz hinzufügen, Hintergrundfarbe ändern und von CAppDialog/CWndClient übernehmen

Die im Artikel verwendeten Programme:

#
 Name
Typ 
Beschreibung 
1 Order.mqh  Klassenbibliothek  Klasse für das Speichern von Informationen über einen Trade
2 OrdersCollection.mqh  Klassenbibliothek  Klasse der Sammlung von Trades
3 StatisticsPanel.mqh  Klassenbibliothek  Klasse des grafischen Interfaces
4 SignalStatistics.mq5  Indikator  Code des Indikators für die Analyse von Trades


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

Beigefügte Dateien |
MQL5.zip (470.5 KB)
Mit der Monte-Carlo-Methode Handelsstrategien optimieren Mit der Monte-Carlo-Methode Handelsstrategien optimieren

Bevor wir einen Roboter auf einem Handelskonto starten, testen und optimieren wir ihn in der Regel anhand der Kurshistorie. Es stellt sich jedoch die berechtigte Frage: Wie können uns die Ergebnisse der Vergangenheit in Zukunft helfen? Der Artikel beschreibt die Anwendung der Monte-Carlo-Methode, um benutzerdefinierte Kriterien für die Optimierung der Handelsstrategie zu erstellen. Zusätzlich werden die EA-Stabilitätskriterien berücksichtigt.

Ein Expert Advisor mit GUI: Erstellen des Panels (Teil I) Ein Expert Advisor mit GUI: Erstellen des Panels (Teil I)

Trotz der Tatsache, dass viele Händler immer noch den manuellen Handel bevorzugen, ist es kaum möglich, die Automatisierung von Routineoperationen vollständig zu vermeiden. Der Artikel zeigt ein Beispiel für die Entwicklung eines Expert Advisor mit Signalen von mehreren Symbolen für den manuellen Handel.

Vergleichende Analyse von 10 Handelsstrategien für Seitwärtsbewegungen Vergleichende Analyse von 10 Handelsstrategien für Seitwärtsbewegungen

Der Artikel untersucht die Vor- und Nachteile des Handels in Seitwärtsbewegungen. Die zehn in diesem Artikel entwickelten und getesteten Strategien basieren auf der Verfolgung von Preisbewegungen innerhalb eines Kanals. Jede Strategie ist mit einem Filtermechanismus ausgestattet, der darauf abzielt, falsche Markteintrittssignale zu vermeiden.

Die Visualisierung von Optimierungsergebnissen nach dem ausgewählten Kriterium Die Visualisierung von Optimierungsergebnissen nach dem ausgewählten Kriterium

Im Artikel wird die MQL-Anwendung für die Arbeit mit Optimierungsergebnissen weiter entwickelt. Diesmal wird ein Beispiel gezeigt, wenn die Tabelle der besten Ergebnisse bereits nach der Optimierung der Parameter gebildet werden kann, indem man ein anderes Kriterium über das grafische Interface angibt.