Erstellen einer Anzeigetafel unter Verwendung der Klassen aus der Standardbibliothek und Google Chart API

Евгений | 12 Januar, 2016


Einleitung

Um den Programmierern, die mit MQL5 arbeiten, das Leben zu erleichtern, haben die Entwickler eine Standardbibliothek angelegt, die nahezu alle Programmierschnittstellenfunktionen von MQL5 abdeckt und die Arbeit mit ihnen leichter und bequemer macht. In diesem Beitrag wird versucht eine Anzeigetafel mit möglichst vielen der von der Standardbibliothek genutzten Klassen zu erstellen.


1. Übersicht über die Klassen der Standardbibliothek

Um was genau handelt es sich bei dieser Bibliothek? Im Bereich Dokumente der Webseite wird ausgeführt, dass sie sich aus folgenden Bestandteilen zusammensetzt:

Die Dateien mit den Kürzeln aller Klassen befinden sich in dem Ordner MQL5/Erfassen (MQL5/Include). Wenn das Bibliothekskürzel angezeigt wird, werden Sie feststellen, dass es nur die Klassen angibt, nicht jedoch die Funktionen. Folglich müssen Sie, um es zu verwenden, etwas von objektorientierter Programmierung (OOP) verstehen. 

Alle Bibliotheksklassen (außer den handelsbezogenen) stammen von der Basisklasse CObject ab. Um das zu zeigen, wollen wir versuchen, eine Schematische Darstellung der Klassen anzulegen, da wir über alles verfügen, was dazu erforderlich ist - die Ausgangsklasse und ihre Nachfolger. Weil es sich bei MQL5 im Grunde um eine Teilmenge von C++ handelt, lassen Sie uns zur automatischen Erzeugung des Schaubildes das UML-Werkzeug Rational Rose von IBM verwenden, denn es bietet Möglichkeiten für das Reverse Engineering von C++-Projekten.

 

Abbildung 1. Schematische Darstellung der Klassen der Standardbibliothek

Wir verzichten wegen der unübersichtlichen Darstellungen, die dabei herauskommen würden, auf die Anzeige der Eigenschaften und Methoden der Klassen. Wir verzichten auch auf Zusammenziehungen, da diese für uns nicht von Bedeutung sind. In der Folge bleiben uns nur Verallgemeinerungen (Nachfolgevorgänge), die uns ermöglichen herauszufinden, welche Eigenschaften und Methoden die Klassen erhalten.

Wie das Schaubild zeigt, besitzt jeder Bestandteil der Bibliothek, der mit Reihen, Dateien, Diagrammen, grafischen Objekten und Arrays arbeitet, seine eigene Ausgangsklasse (CString, CFile, CChart, CChartObject bzw. CArray), die er von dem CObject übernommen hat. Die Ausgangsklasse für die Arbeit mit Indikatoren CIndicator und ihre Hilfsklasse CIndicators wurden von CArrayObj übernommen, während der Zugriff auf die Indikatorpufferklasse CIndicatorBuffer von der Klasse CArrayDouble abstammt.

Die karminrote Farbe in dem Schaubild kennzeichnet die in Wirklichkeit nicht vorhandenen Klassen, Indikatoren, Arrays und Diagrammobjekte, es handelt sich bei ihnen um gesetzte Rahmen, die Klassen für die Arbeit mit Indikatoren, Arrays und grafischen Objekten umfassen. Da es eine große Zahl von ihnen gibt und sie alle von einem einzigen Vorgänger abstammen, habe ich mir bei einigen eine Vereinfachung erlaubt, um die Darstellung nicht zu überfrachten. Zum Beispiel enthält der Indikator CiDEMA, CiStdDev usw.

Es sei zudem darauf hingewiesen, dass die schematische Darstellung der Klassen auch mithilfe der automatischen Erstellungsfunktion des Dokumentationssystems Doxygen angelegt werden kann. Es ist etwas einfacher, das mit diesem System zu tun als mit Rational Rose. Weitere Informationen zu Doxygen bietet der Artikel: Automatisch erstellte Dokumentation für MQL5.


2. Aufgabenstellung

Versuchen wir, eine Anzeigetafel mit der größtmöglichen Zahl von Standardbibliotheksklassen anzulegen.

Was soll auf der Tafel angezeigt werden? Etwas in der Art einer ausführlichen Anzeige aus MetaTrader 5, d. h.:

Abbildung 2. Das Erscheinungsbild der ausführlichen Anzeige

Abbildung 2. Das Erscheinungsbild der ausführlichen Anzeige

Wie wir sehen können, werden eine Kontenverlaufskurve sowie einige Umsatzwerte angezeigt. Weitere Angaben zu den Methoden für die Berechnung dieser Indikatoren liefert der Artikel: Was bedeuten Zahlen im Bericht über den Expertentest.

Da die Anzeigetafel ausschließlich zu Informationszwecken verwendet wird und keine Handelsaktivitäten ausführt, wird es am besten sein, sie als Indikator in einem gesonderten Fenster zu realisieren, um ein Schließen des echten Diagramms zu vermeiden. Außerdem ermöglicht die Unterbringung in einem Unterfenster eine einfachere Größenanpassung bis hin zum Schließen der Tafel mit einer einzigen Mausbewegung.

Sie möchten die Anzeige möglicherweise mit einem Kreisdiagramm vervollständigen, das die Anzahl der hinsichtlich des betreffenden Instruments getätigten Transaktionen im Verhältnis zu der Gesamtzahl der Transaktionen wiedergibt.


3. Gestaltung der Benutzeroberfläche 

Wir haben unsere Ziele bestimmt, jetzt brauchen wir eine ausführliche Anzeige im Unterfenster des Hauptdiagramms.

Wir setzen unsere Anzeigetafel als Klasse um. Fangen wir an:

//+------------------------------------------------------------------+
///The Board class
//+------------------------------------------------------------------+
class Board
  {
//protected data
protected:
///number of the sub-window where the board will be stored
   int               wnd;             
///array with the deals data   
   CArrayObj        *Data;
///array with the balance data   
   CArrayDouble      ChartData;       
///array with elements of the interface   
   CChartObjectEdit  cells[10][6];    
///object for working with the chart   
   CChart            Chart;           
///object for working with the balance chart
   CChartObjectBmpLabel BalanceChart; 
///object for working with the pie chart
   CChartObjectBmpLabel PieChart;     
///data for the pie chart
   PieData          *pie_data;
//private data and methods
private:
   double            net_profit;      //these variables will store the calculated characteristics
   double            gross_profit;
   double            gross_loss;
   double            profit_factor;
   double            expected_payoff;
   double            absolute_drawdown;
   double            maximal_drawdown;
   double            maximal_drawdown_pp;
   double            relative_drawdown;
   double            relative_drawdown_pp;
   int               total;
   int               short_positions;
   double            short_positions_won;
   int               long_positions;
   double            long_positions_won;
   int               profit_trades;
   double            profit_trades_pp;
   int               loss_trades;
   double            loss_trades_pp;
   double            largest_profit_trade;
   double            largest_loss_trade;
   double            average_profit_trade;
   double            average_loss_trade;
   int               maximum_consecutive_wins;
   double            maximum_consecutive_wins_usd;
   int               maximum_consecutive_losses;
   double            maximum_consecutive_losses_usd;
   int               maximum_consecutive_profit;
   double            maximum_consecutive_profit_usd;
   int               maximum_consecutive_loss;
   double            maximum_consecutive_loss_usd;
   int               average_consecutive_wins;
   int               average_consecutive_losses;

   ///method of obtaining data about the deals and the balance
   void              GetData();

   ///method of calculating the characteristics
   void              Calculate();
   
   ///method of chart construction
   void              GetChart(int X_size,int Y_size,string request,string file_name);
   
   ///method of request to Google Charts API
   string            CreateGoogleRequest(int X_size,int Y_size,bool type);
   
  ///method of obtaining the optimum font size
   int               GetFontSize(int x,int y);
   string            colors[12];  //array with text presentation of colors
//public methods
public:
///constructor
   void              Board();    
///destructor         
   void             ~Board();    
///method for board update
   void              Refresh();  
///method for creating interface elements   
   void              CreateInterface(); 
  };

Die geschützten Daten der Klasse sind die Elemente der Benutzeroberfläche sowie die Angaben zum jeweiligen Abschluss, zum Kontostand und die Daten des Kreisdiagramms (die Klasse PieData wird weiter hinten besprochen). Die Handelsindikatoren und einige Methoden sind nicht öffentlich. Sie sind nicht öffentlich, weil der Anwender keinen unmittelbaren Zugriff haben soll, sie innerhalb der Klasse berechnet werden und nur durch Aufrufen der entsprechenden öffentlichen Methode erfasst werden können.

Auch die Methoden der Erstellung der Benutzeroberfläche und der Berechnung von Indikatoren sind nicht öffentlich, da hier eine strenge Abfolge von Methodenabrufen eingehalten werden muss. Es ist zum Beispiel unmöglich die Indikatoren zu berechnen, ohne die Daten für die Berechnung zu haben, oder die Benutzeroberfläche zu aktualisieren, ohne sie vorher anlegen zu müssen. Also, lassen wir Anwender „sich nicht selbst ins Knie schießen“. 

Machen wir uns jetzt sofort an die Konstruktoren und Destruktoren einer Klasse, damit wir nicht später auf sie zurückkommen müssen:

//+------------------------------------------------------------------+
///Constructor
//+------------------------------------------------------------------+
void Board::Board()
  {
   Chart.Attach();                               //attach the current chart to the class instance
   wnd=ChartWindowFind(Chart.ChartId(),"IT");    //find the indicator window
   Data = new CArrayObj;                         //creating the CArrayObj class instance
   pie_data=new PieData;                         //creating the PieData class instance
   //fill colors array
   colors[0]="003366"; colors[1]="00FF66"; colors[2]="990066";
   colors[3]="FFFF33"; colors[4]="FF0099"; colors[5]="CC00FF";
   colors[6]="990000"; colors[7]="3300CC"; colors[8]="000033";
   colors[9]="FFCCFF"; colors[10]="CC6633"; colors[11]="FF0000";
  }
//+------------------------------------------------------------------+
///Destructor
//+------------------------------------------------------------------+
void Board::~Board()
  {
   if(CheckPointer(Data)!=POINTER_INVALID) delete Data;   //delete the deals data
   if(CheckPointer(pie_data)!=POINTER_INVALID) delete pie_data;
   ChartData.Shutdown();    //and balance data
   Chart.Detach();          //detach from the chart
   for(int i=0;i<10;i++)    //delete all interface elements
      for(int j=0;j<6;j++)
         cells[i][j].Delete();
   BalanceChart.Delete();   //delete the balance chart
   PieChart.Delete();       //and pie chart
  }

Im Konstruktor binden wir ein Objekt der Art CChart mithilfe der Methode Attach() an das aktuelle Diagramm. Die in dem Destruktor aufgerufene Methode Detach() löst das Diagramm von dem Objekt. Ein Datenobjekt, das auf ein Objekt vom Typ CArrayObj verweist, erhält die mithilfe des Befehls new dynamisch angelegte Adresse des Objektes, die in dem Destruktor mithilfe des Befehls delete entfernt wird. Vergessen Sie nicht, vor dem Löschen mithilfe der Verweisprüfung CheckPointer() zu prüfen, ob ein Objekt vorhanden ist, andernfalls erscheint eine Fehlermeldung.

Weitere Angaben zur Klasse CArrayObj folgen später. Die Methode Shutdown() der Klasse CArrayDouble, die wie alle anderen Klassen von der Klasse CArray übernommen wurde (siehe Klassenschaubild), löscht und leert den von dem Objekt besetzten Speicherplatz. Mit der Methode Delete() der Nachfolger der Klasse CChartObject wird das Objekt aus dem Diagramm entfernt.

Somit weist der Konstruktor Speicherplatz zu, während der Destruktor ihn befreit und die von der Klasse erzeugten grafischen Objekte entfernt. 

Kümmern wir uns jetzt um die Benutzeroberfläche. Wie oben ausgeführt legt die Methode CreateInterface() eine Benutzeroberfläche für die Tafel an:

//+------------------------------------------------------------------+
///CreateInterface function
//+------------------------------------------------------------------+
void Board::CreateInterface()
  {
   //retrieve the width
   int x_size=Chart.WidthInPixels();
   //and the height of the indicator window
   int y_size=Chart.GetInteger(CHART_HEIGHT_IN_PIXELS,wnd);
   
    //calculate, how much space will the balance chart take up
   double chart_border=y_size*(1.0-(Chart_ratio/100.0));

   if(Chart_ratio<100)//if the balance chart is taking up the entire table
     {
      for(int i=0;i<10;i++)//create columns
        {
         for(int j=0;j<6;j++)//and rows
           {
            cells[i][j].Create(Chart.ChartId(),"InfBoard "+IntegerToString(i)+" "+IntegerToString(j),
                               wnd,j*(x_size/6.0),i*(chart_border/10.0),x_size/6.0,chart_border/10.0);
            //set selectable property to false
            cells[i][j].Selectable(false);
            //set text as read only
            cells[i][j].ReadOnly(true);
            //set font size
            cells[i][j].FontSize(GetFontSize(x_size/6.0, chart_border/10.0));
            cells[i][j].Font("Arial");    //font name
            cells[i][j].Color(text_color);//font color
           }
        }
     }

   if(Chart_ratio>0)//if the balance chart is required
     {
      //create a balance chart
      BalanceChart.Create(Chart.ChartId(), "InfBoard chart", wnd, 0, chart_border);
      //set selectable property to false
      BalanceChart.Selectable(false);
      //create a pie chart
      PieChart.Create(Chart.ChartId(), "InfBoard pie_chart", wnd, x_size*0.75, chart_border);
      PieChart.Selectable(false);//set selectable property to false
     }

   Refresh();//refresh the board
  }

Zur kompakten Anordnung aller Elemente ermitteln Sie zunächst mithilfe der Methoden WidthInPixels() und GetInteger() der Klasse CChart Länge und Breite des Indikator-Unterfensters, in dem die Tafel untergebracht werden soll. Anschließend legen wir mithilfe der Methode Create() der Klasse CChartObjectEdit (sie erzeugt das „Eingabefeld“) die Zellen an, die die Werte der Indikatoren enthalten sollen, alle Nachfolger verfügen über diese Methode von CChartObject.

Beachten Sie, wie bequem die Verwendung der Standardbibliothek für derartige Vorgänge ist. Ohne sie müssten wir jedes Objekt mithilfe der Funktion ObjectCreate erstellen, und die Objekteigenschaften mit Funktionen wie ObjectSet festlegen, was zu Redundanzen bei der Programmierung führen würde. Und wenn die Objekteigenschaften später geändert werden sollen, müssten alle Objektbezeichnungen sorgfältig überprüft werden, um ein Durcheinander zu vermeiden. Jetzt können wir einfach ein Array mit grafischen Objekten anlegen und es durchsuchen, wenn wir möchten.

Außerdem können wir die Objekteigenschaften mithilfe einer einzigen Funktion abrufen/festlegen, wenn sie über prall gefüllte Erstellhilfen der Klasse verfügt wie z. B. die Methode Color() der Klasse CChartObject. Wird sie mit den Parametern aufgerufen, legt sie sie fest, ohne Parameter stellt sie die Objektfarbe wieder her. Platzieren Sie das Kreisdiagramm unmittelbar neben der Kontenverlaufskurve, es wird bis zu einem Viertel der Fensterbreite einnehmen.

Die Methode Refresh() aktualisiert die Anzeigetafel. Worin besteht diese Aktualisierung? Wir müssen die Indikatoren zusammenzählen, sie in die grafischen Objekte eingeben, und die Größe der Anzeigetafel anpassen, wenn die Größe des Fensters, in dem sie untergebracht ist, geändert wurde. Die Tafel sollte die gesamte freie Fläche des Fensters einnehmen.

//+------------------------------------------------------------------+
///Function of the board updating
//+------------------------------------------------------------------+
void Board::Refresh()
  {
   //check the server connection status
   if(!TerminalInfoInteger(TERMINAL_CONNECTED)) {Alert("No connection with the trading server!"); return;}
   //check the permission for importing functions from DLL
   if(!TerminalInfoInteger(TERMINAL_DLLS_ALLOWED)) {Alert("DLLs are prohibited!"); return;}
   //calculate the characteristics
   Calculate();
   //retrieve the width
   int x_size=Chart.WidthInPixels();
   //and the height of the indicator window
   int y_size=Chart.GetInteger(CHART_HEIGHT_IN_PIXELS,wnd);
   //calculate how much space the balance chart will take up
   double chart_border=y_size*(1.0-(Chart_ratio/100.0));

   string captions[10][6]= //array with signatures of interface elements
     {
        {"Total Net Profit:"," ","Gross Profit:"," ","Gross Loss:"," "},
        {"Profit Factor:"," ","Expected Payoff:"," ","",""},
        {"Absolute Drawdown:"," ","Maximal Drawdown:"," ","Relative Drawdown:"," "},
        {"Total Trades:"," ","Short Positions (won %):"," ","Long Positions (won %):"," "},
        {"","","Profit Trades (% of total):"," ","Loss trades (% of total):"," "},
        {"Largest","","profit trade:"," ","loss trade:"," "},
        {"Average","","profit trade:"," ","loss trade:"," "},
        {"Maximum","","consecutive wins ($):"," ","consecutive losses ($):"," "},
        {"Maximal","","consecutive profit (count):"," ","consecutive loss (count):"," "},
        {"Average","","consecutive wins:"," ","consecutive losses:"," "}
     };

   //put the calculated characteristics into the array
   captions[0][1]=DoubleToString(net_profit, 2);
   captions[0][3]=DoubleToString(gross_profit, 2);
   captions[0][5]=DoubleToString(gross_loss, 2);

   captions[1][1]=DoubleToString(profit_factor, 2);
   captions[1][3]=DoubleToString(expected_payoff, 2);

   captions[2][1]=DoubleToString(absolute_drawdown, 2);
   captions[2][3]=DoubleToString(maximal_drawdown, 2)+"("+DoubleToString(maximal_drawdown_pp, 2)+"%)";
   captions[2][5]=DoubleToString(relative_drawdown_pp, 2)+"%("+DoubleToString(relative_drawdown, 2)+")";

   captions[3][1]=IntegerToString(total);
   captions[3][3]=IntegerToString(short_positions)+"("+DoubleToString(short_positions_won, 2)+"%)";
   captions[3][5]=IntegerToString(long_positions)+"("+DoubleToString(long_positions_won, 2)+"%)";

   captions[4][3]=IntegerToString(profit_trades)+"("+DoubleToString(profit_trades_pp, 2)+"%)";
   captions[4][5]=IntegerToString(loss_trades)+"("+DoubleToString(loss_trades_pp, 2)+"%)";

   captions[5][3]=DoubleToString(largest_profit_trade, 2);
   captions[5][5]=DoubleToString(largest_loss_trade, 2);

   captions[6][3]=DoubleToString(average_profit_trade, 2);
   captions[6][5]=DoubleToString(average_loss_trade, 2);

   captions[7][3]=IntegerToString(maximum_consecutive_wins)+"("+DoubleToString(maximum_consecutive_wins_usd, 2)+")";
   captions[7][5]=IntegerToString(maximum_consecutive_losses)+"("+DoubleToString(maximum_consecutive_losses_usd, 2)+")";

   captions[8][3]=DoubleToString(maximum_consecutive_profit_usd, 2)+"("+IntegerToString(maximum_consecutive_profit)+")";
   captions[8][5]=DoubleToString(maximum_consecutive_loss_usd, 2)+"("+IntegerToString(maximum_consecutive_loss)+")";

   captions[9][3]=IntegerToString(average_consecutive_wins);
   captions[9][5]=IntegerToString(average_consecutive_losses);

   if(Chart_ratio<100) //if the balance chart doesn't take up the entire table
     {
      for(int i=0;i<10;i++) //go through the interface elements
        {
         for(int j=0;j<6;j++)
           {
            //specify the position
            cells[i][j].X_Distance(j*(x_size/6.0));
            cells[i][j].Y_Distance(i*(chart_border/10.0));
            //the size
            cells[i][j].X_Size(x_size/6.0);
            cells[i][j].Y_Size(chart_border/10.0);
            //the text
            cells[i][j].SetString(OBJPROP_TEXT,captions[i][j]);
            //and font size
            cells[i][j].FontSize(GetFontSize(x_size/6.0,chart_border/10.0));
           }
        }
     }

   if(Chart_ratio>0)//if the balance chart is required
     {
      //refresh the balance chart
      int X=x_size*0.75,Y=y_size-chart_border;
      //get the chart
      GetChart(X,Y,CreateGoogleRequest(X,Y,true),"board_balance_chart");
      //set its position
      BalanceChart.Y_Distance(chart_border);
      //specify file names
      BalanceChart.BmpFileOn("board_balance_chart.bmp");
      BalanceChart.BmpFileOff("board_balance_chart.bmp");
      //refresh the pie chart
      X=x_size*0.25;
      //get the chart
      GetChart(X,Y,CreateGoogleRequest(X,Y,false),"pie_chart");
      //set its new position
      PieChart.Y_Distance(chart_border);
      PieChart.X_Distance(x_size*0.75);
      //specify file names
      PieChart.BmpFileOn("pie_chart.bmp");
      PieChart.BmpFileOff("pie_chart.bmp");
     }

   ChartRedraw(); //redraw the chart
  }

Es gibt massenhaft Kürzel, wie bei der Methode CreateInterface() berechnet zunächst die Funktion Calculate() die Indikatoren, die dann in die grafischen Objekte eingegeben werden, während gleichzeitig die Objektgrößen mittels der Methoden X_Size() und Y_Size() in Fenstergrößen umgewandelt werden. Die Methoden X_Distance und Y_Distance verändern die Lage des Objekts.

Achten Sie auf die Funktion GetFontSize(), sie wählt eine Schriftgröße, die den Text nach einer Größenänderung nicht über die Ränder seines „Behälters“ treten und im umgekehrten Fall nicht zu klein werden lässt.

Sehen wir uns diese Funktion etwas genauer an:

//import DLL function for string metrics
#import "String_Metrics.dll" 
void GetStringMetrics(int font_size,int &X,int &Y);
#import

//+------------------------------------------------------------------+
///Function of determining the optimum font size
//+------------------------------------------------------------------+
int Board::GetFontSize(int x,int y)
  {
   int res=8;
   for(int i=15;i>=1;i--)//go through the different font sizes
     {
      int X,Y; //here we input the line metrics
      //determine the metrics
      GetStringMetrics(i,X,Y);
      //if the line fits the set borders - return the font size
      if(X<=x && Y<=y) return i;
     }
   return res;
  }

Die Funktion GetStringMetrics() wird aus der oben dargestellten DLL importiert, deren Programmierung in dem Archiv DLL_Sources.zip zu finden ist und gegebenenfalls modifiziert werden kann. Ich glaube, es käme ganz gelegen, wenn Sie sich dafür entscheiden würden, in dem Projekt Ihre eigene Benutzeroberfläche zu entwerfen.

Die Benutzeroberfläche ist jetzt fertig, wenden wir uns also der Berechnung der Handelsindikatoren zu.


4. Berechnung der Handelsindikatoren

Die Methode Calculate() führt die Berechnungen aus.

Wir benötigen jedoch ebenfalls die Methode GetData(), um die erforderlichen Daten zu erhalten:

//+------------------------------------------------------------------+
///Function of receiving the deals and balance data
//+------------------------------------------------------------------+
void Board::GetData()
  {
   //delete old data
   Data.Shutdown();
   ChartData.Shutdown();
   pie_data.Shutdown();
   //prepare all the deals history
   HistorySelect(0,TimeCurrent()); 
   CAccountInfo acc_inf;   //object for work with account
   //calculate the balance
   double balance=acc_inf.Balance();
   double store=0; //balance
   long_positions=0;
   short_positions=0;
   long_positions_won=0;
   short_positions_won=0;
   for(int i=0;i<HistoryDealsTotal();i++) //go through all of the deals in the history

     {
      CDealInfo deal;  //the information about the deals will be stored here
      deal.Ticket(HistoryDealGetTicket(i));//get deal ticket
      //if the trade had a financial result (exit of the market)
      if(deal.Ticket()>=0 && deal.Entry()==DEAL_ENTRY_OUT)
        {
         pie_data.Add(deal.Symbol()); //add data for the pie chart
         //check for the symbol 
         if(!For_all_symbols && deal.Symbol()!=Symbol()) continue;
         double profit=deal.Profit(); //retrieve the trade profit
         profit+=deal.Swap();         //swap
         profit+=deal.Commission();   //commission
         store+=profit;               //cumulative profit
         Data.Add(new CArrayDouble);  //add new element to the array
         ((CArrayDouble *)Data.At(Data.Total()-1)).Add(profit);  //and data
         ((CArrayDouble *)Data.At(Data.Total()-1)).Add(deal.Type());
        }
     }

   //calculate the initial deposit
   double initial_deposit=(balance-store);
   for(int i=0;i<Data.Total();i++) //go through the prepared trades
     {
      //calculate the balance value
      initial_deposit+=((CArrayDouble *)Data.At(i)).At(0);
      ChartData.Add(initial_deposit); //and put it to the array
     }
  }

Denken wir zunächst über die Methode zur Speicherung der Daten nach. Die Standardbibliothek bietet die Klassen von Datenstrukturen, die Ihnen den Verzicht auf die Verwendung von Arrays ermöglicht. Wir benötigen ein zweidimensionales Array, in dem wir die Daten zu Gewinnen und den Arten der Transaktionen in einem Verlauf speichern. Die Standardbibliothek bietet jedoch keine gesonderten Klassen für den Aufbau zweidimensionaler Arrays, obwohl es dort die Klassen CArrayDouble (Arrays mit Daten des Typs Double) und CArrayObj (dynamische Arrays mit Zeigern für Instanzen der Klasse CObject sowie deren Nachfolger) gibt. Das heißt, dass wir ein Array aus Datenfeldern des Typs Double erstellen können, und das ist genau das, was hier gemacht wird. 

Natürlich sehen Aussagen wie ((CArrayDouble *) Data.At (Data.Total () - 1 )). Add (profit) nicht so nett aus wie data [i] [j] = profit, aber das nur auf den ersten Blick. Letztlich beraubt uns das schlichte Deklarieren eines Arrays ohne Verwendung der Klassen der Standardbibliothek solcher Vorteile wie der integrierten Speicherplatzverwaltung, der Möglichkeit, ein anderes Array einzufügen, Arrays zu vergleichen, Elemente zu suchen usw. Somit befreit uns der Einsatz von Klassen zur Organisation des Speicherplatzes von der Notwendigkeit das „Überlaufen“ des Arrays zu überwachen, und verschafft uns zahlreiche nützliche Werkzeuge. 

Die Methode Total() der Klasse CArray (s. Abb. 1.) gibt die Anzahl der Elemente in dem Array wieder, die Methode Add() fügt sie hinzu und die Methode At() zeigt die Elemente an.

Da wir beschlossen haben, ein Kreisdiagramm anzulegen, um die Anzahl der Geschäftsabschlüsse für Währungspaarkürzel darzustellen, müssen wir die erforderlichen Daten erfassen.

Wir werden zur Erfassung dieser Daten eine Hilfsklasse programmieren:

//+------------------------------------------------------------------+
///The Pie chart class
//+------------------------------------------------------------------+
class PieData
  {
protected:
///number of deals per symbol
   CArrayInt         val;   
///symbols
   CArrayString      symb;  
public:
///delete the data
   bool Shutdown()          
     {
      bool res=true;
      res&=val.Shutdown();
      res&=symb.Shutdown();
      return res;
     }
///search for a sting in the array
   int Search(string str)   
     {  //check all array elements
      for(int i=0;i<symb.Total();i++)
         if(symb.At(i)==str) return i;
      return -1;
     }
///add new data
   void Add(string str)    
     {
      int symb_pos=Search(str);//determine symbol position in the array
      if(symb_pos>-1)
         val.Update(symb_pos,val.At(symb_pos)+1);//update the deals data
      else //if there isn't such a symbol yet
        {
         symb.Add(str); //add it
         val.Add(1);
        }
     }

   int Total() const {return symb.Total();}
   int Get_val(int pos) const {return val.At(pos);}
   string Get_symb(int pos) const {return symb.At(pos);}
  };

Es ist nicht immer so, dass die Klassen der Standardbibliothek in der Lage sind, uns die für die Arbeit erforderlichen Methoden zu verschaffen. In diesem Beispiel ist die Methode Search() der Klasse CArrayString ungeeignet, weil wir, um sie anwenden zu können, zunächst das Array aussortieren müssen, wodurch die Datenstruktur verletzt würde. Deshalb mussten wir unsere eigene Methode programmieren.

Die Berechnung der Handelsdaten ist in der Methode Calculate() enthalten:

//+------------------------------------------------------------------+
///Calculation of characteristics
//+------------------------------------------------------------------+
void Board::Calculate()
  {
   //get the data
   GetData();
   //zero all characteristics
   gross_profit=0;
   gross_loss=0;
   net_profit=0;
   profit_factor=0;
   expected_payoff=0;
   absolute_drawdown=0;
   maximal_drawdown_pp=0;
   maximal_drawdown=0;
   relative_drawdown=0;
   relative_drawdown_pp=0;
   total=Data.Total();
   long_positions=0;
   long_positions_won=0;
   short_positions=0;
   short_positions_won=0;
   profit_trades=0;
   profit_trades_pp=0;
   loss_trades=0;
   loss_trades_pp=0;
   largest_profit_trade=0;
   largest_loss_trade=0;
   average_profit_trade=0;
   average_loss_trade=0;
   maximum_consecutive_wins=0;
   maximum_consecutive_wins_usd=0;
   maximum_consecutive_losses=0;
   maximum_consecutive_losses_usd=0;
   maximum_consecutive_profit=0;
   maximum_consecutive_profit_usd=0;
   maximum_consecutive_loss=0;
   maximum_consecutive_loss_usd=0;
   average_consecutive_wins=0;
   average_consecutive_losses=0;

   if(total==0) return; //there isn't deals - return from the function
   double max_peak=0,min_peak=0,tmp_balance=0;
   int max_peak_pos=0,min_peak_pos=0;
   int max_cons_wins=0,max_cons_losses=0;
   double max_cons_wins_usd=0,max_cons_losses_usd=0;
   int avg_win=0,avg_loss=0,avg_win_cnt=0,avg_loss_cnt=0;

   for(int i=0; i<total; i++)
     {
      double profit=((CArrayDouble *)Data.At(i)).At(0); //get profit
      int deal_type=((CArrayDouble *)Data.At(i)).At(1); //and deal type
      switch(deal_type) //check deal type
        {
         //and calculate number of long and short positions
         case DEAL_TYPE_BUY: {long_positions++; if(profit>=0) long_positions_won++; break;}
         case DEAL_TYPE_SELL: {short_positions++; if(profit>=0) short_positions_won++; break;}
        }

      if(profit>=0)//the deal is profitable
        {
         gross_profit+=profit; //gross profit
         profit_trades++;      //number of profit deals
         //the largest profitable trade and the largest profitable series
         if(profit>largest_profit_trade) largest_profit_trade=profit;

         if(maximum_consecutive_losses<max_cons_losses || 
            (maximum_consecutive_losses==max_cons_losses && maximum_consecutive_losses_usd>max_cons_losses_usd))
           {
            maximum_consecutive_losses=max_cons_losses;
            maximum_consecutive_losses_usd=max_cons_losses_usd;
           }
         if(maximum_consecutive_loss_usd>max_cons_losses_usd || 
            (maximum_consecutive_loss_usd==max_cons_losses_usd && maximum_consecutive_losses<max_cons_losses))
           {
            maximum_consecutive_loss=max_cons_losses;
            maximum_consecutive_loss_usd=max_cons_losses_usd;
           }
         //average profit per deal
         if(max_cons_losses>0) {avg_loss+=max_cons_losses; avg_loss_cnt++;}
         max_cons_losses=0;
         max_cons_losses_usd=0;
         max_cons_wins++;
         max_cons_wins_usd+=profit;
        }
      else //deal is losing
        {
         gross_loss-=profit; //cumulative profit
         loss_trades++;      //number of losing deals
         //the most unprofitable deal and the most unprofitable series
         if(profit<largest_loss_trade) largest_loss_trade=profit;
         if(maximum_consecutive_wins<max_cons_wins || 
            (maximum_consecutive_wins==max_cons_wins && maximum_consecutive_wins_usd<max_cons_wins_usd))
           {
            maximum_consecutive_wins=max_cons_wins;
            maximum_consecutive_wins_usd=max_cons_wins_usd;
           }
         if(maximum_consecutive_profit_usd<max_cons_wins_usd || 
            (maximum_consecutive_profit_usd==max_cons_wins_usd && maximum_consecutive_profit<max_cons_wins))
           {
            maximum_consecutive_profit=max_cons_wins;
            maximum_consecutive_profit_usd=max_cons_wins_usd;
           }
         //average lose per deal
         if(max_cons_wins>0) {avg_win+=max_cons_wins; avg_win_cnt++;}
         max_cons_wins=0;
         max_cons_wins_usd=0;
         max_cons_losses++;
         max_cons_losses_usd+=profit;
        }

      tmp_balance+=profit; //absolute drawdown calculation
      if(tmp_balance>max_peak) {max_peak=tmp_balance; max_peak_pos=i;}
      if(tmp_balance<min_peak) {min_peak=tmp_balance; min_peak_pos=i;}
      if((max_peak-min_peak)>maximal_drawdown && min_peak_pos>max_peak_pos) maximal_drawdown=max_peak-min_peak;
     }
   //maximal drawdown calculation
   double min_peak_rel=max_peak;
   tmp_balance=0;
   for(int i=max_peak_pos;i<total;i++)
     {
      double profit=((CArrayDouble *)Data.At(i)).At(0);
      tmp_balance+=profit;
      if(tmp_balance<min_peak_rel) min_peak_rel=tmp_balance;
     }
   //relative drawdown calculation
   relative_drawdown=max_peak-min_peak_rel;
   //net profit
   net_profit=gross_profit-gross_loss;
   //profit factor
   profit_factor=(gross_loss!=0) ?  gross_profit/gross_loss : gross_profit;
   //expected payoff
   expected_payoff=net_profit/total;
   double initial_deposit=AccountInfoDouble(ACCOUNT_BALANCE)-net_profit;
   absolute_drawdown=MathAbs(min_peak); 
   //drawdowns
   maximal_drawdown_pp=(initial_deposit!=0) ?(maximal_drawdown/initial_deposit)*100.0 : 0;
   relative_drawdown_pp=((max_peak+initial_deposit)!=0) ?(relative_drawdown/(max_peak+initial_deposit))*100.0 : 0;
   
   //profit and losing trade percentage
   profit_trades_pp=((double)profit_trades/total)*100.0;
   loss_trades_pp=((double)loss_trades/total)*100.0;
   
   //average profitable and losing deals
   average_profit_trade=(profit_trades>0) ? gross_profit/profit_trades : 0;
   average_loss_trade=(loss_trades>0) ? gross_loss/loss_trades : 0;
   
   //maximum consecutive losses
   if(maximum_consecutive_losses<max_cons_losses || 
      (maximum_consecutive_losses==max_cons_losses && maximum_consecutive_losses_usd>max_cons_losses_usd))
     {
      maximum_consecutive_losses=max_cons_losses;
      maximum_consecutive_losses_usd=max_cons_losses_usd;
     }
   if(maximum_consecutive_loss_usd>max_cons_losses_usd || 
      (maximum_consecutive_loss_usd==max_cons_losses_usd && maximum_consecutive_losses<max_cons_losses))
     {
      maximum_consecutive_loss=max_cons_losses;
      maximum_consecutive_loss_usd=max_cons_losses_usd;
     }

   if(maximum_consecutive_wins<max_cons_wins || 
      (maximum_consecutive_wins==max_cons_wins && maximum_consecutive_wins_usd<max_cons_wins_usd))
     {
      maximum_consecutive_wins=max_cons_wins;
      maximum_consecutive_wins_usd=max_cons_wins_usd;
     }
   if(maximum_consecutive_profit_usd<max_cons_wins_usd || 
      (maximum_consecutive_profit_usd==max_cons_wins_usd && maximum_consecutive_profit<max_cons_wins))
     {
      maximum_consecutive_profit=max_cons_wins;
      maximum_consecutive_profit_usd=max_cons_wins_usd;
     }
   //average loss and profit
   if(max_cons_losses>0) {avg_loss+=max_cons_losses; avg_loss_cnt++;}
   if(max_cons_wins>0) {avg_win+=max_cons_wins; avg_win_cnt++;}
   average_consecutive_wins=(avg_win_cnt>0) ? round((double)avg_win/avg_win_cnt) : 0;
   average_consecutive_losses=(avg_loss_cnt>0) ? round((double)avg_loss/avg_loss_cnt) : 0;
   
   //number of profitable long and short positions
   long_positions_won=(long_positions>0) ?((double)long_positions_won/long_positions)*100.0 : 0;
   short_positions_won=(short_positions>0) ?((double)short_positions_won/short_positions)*100.0 : 0;
  }

5. Verwenden der Programmierschnittstelle von Google Chart zur Erstellung einer Kontenverlaufskurve

Die Diagramm-Programmierschnittstelle Google Chart API ermöglicht Entwicklern die schnelle Erstellung von schematischen Darstellungen verschiedener Art. Google Chart API ist unter der entsprechenden Verknüpfung (URL) auf den Webservern von Google gespeichert, und gibt das Bild bei Empfang einer korrekt formatierten Pfadadresse (URL) als Bild aus.

Die Eigenschaften des Schaubildes (Farben, Überschriften, Achsen, Punkte im Diagramm usw.) werden von der Verknüpfung (URL) angegeben. Das ausgegebene Bild kann in einem Dateisystem oder einer Datenbank gespeichert werden. Das Angenehmste ist dabei, dass Google Chart API kostenlos ist und weder ein Konto noch eine Anmeldung erfordert.  

Die Methode GetChart() ruft das Diagramm von Google ab und speichert es auf einem Datenträger oder der Festplatte:

#import "PNG_to_BMP.dll"//import of DLL with the function of conversion of PNG images to BMP
bool Convert_PNG(string src,string dst);
#import

#import "wininet.dll"//import DLL with the function for working with the internet
int InternetAttemptConnect(int x);
int InternetOpenW(string sAgent,int lAccessType,
                  string sProxyName="",string sProxyBypass="",
                  int lFlags=0);
int InternetOpenUrlW(int hInternetSession,string sUrl,
                     string sHeaders="",int lHeadersLength=0,
                     int lFlags=0,int lContext=0);
int InternetReadFile(int hFile,char &sBuffer[],int lNumBytesToRead,
                     int &lNumberOfBytesRead[]);
int InternetCloseHandle(int hInet);
#import

//+------------------------------------------------------------------+
///Function of creating a balance chart
//+------------------------------------------------------------------+
void Board::GetChart(int X_size,int Y_size,string request,string file_name)
  {
   if(X_size<1 || Y_size<1) return; //too small
   //try to create connection
   int rv=InternetAttemptConnect(0);
   if(rv!=0) {Alert("Error in call of the InternetAttemptConnect()"); return;}
   //initialize the structures
   int hInternetSession=InternetOpenW("Microsoft Internet Explorer", 0, "", "", 0);
   if(hInternetSession<=0) {Alert("Error in call of the InternetOpenW()"); return;}
   //send request
   int hURL=InternetOpenUrlW(hInternetSession, request, "", 0, 0, 0);
   if(hURL<=0) Alert("Error in call of the InternetOpenUrlW()");
   //file with the result
   CFileBin chart_file;
   //let's create it
   chart_file.Open(file_name+".png",FILE_BIN|FILE_WRITE);
   int dwBytesRead[1]; //number of data read
   char readed[1000];  //the data 
   //read the data, returned by server after the request
   while(InternetReadFile(hURL,readed,1000,dwBytesRead))
     {
      if(dwBytesRead[0]<=0) break; //no data - exit
      chart_file.WriteCharArray(readed,0,dwBytesRead[0]); //write data to file
     }
   InternetCloseHandle(hInternetSession);//close connection
   chart_file.Close();//close file
   //******************************
   //prepare the paths for the converter
   CString src;
   src.Assign(TerminalInfoString(TERMINAL_PATH));
   src.Append("\MQL5\Files\\"+file_name+".png");
   src.Replace("\\","\\\\");
   CString dst;
   dst.Assign(TerminalInfoString(TERMINAL_PATH));
   dst.Append("\MQL5\Images\\"+file_name+".bmp");
   dst.Replace("\\","\\\\");
   //convert the file
   if(!Convert_PNG(src.Str(),dst.Str())) Alert("Error in call of the Convert_PNG()");
  }
  

Ausführliche Angaben zur Arbeit mit Online verfügbaren Instrumenten der Programmierschnittstellen von Windows und MQL5 bietet der Artikel: WinInet.dll für den Datenaustausch zwischen Ausgabegeräten über das Internet verwenden. Deshalb werden wir uns damit jetzt nicht befassen. Die importierte Funktion Convert_PNG() habe ich zur Umwandlung von Bildern aus dem Format PNG in BMP programmiert.

Das ist erforderlich, weil Google Chart die Diagramme in den Formaten PNG oder GIF ausgibt, das Objekt für „grafische Etiketten“ jedoch nur Bilder in BMP annimmt. Der Programmcode der entsprechenden Bibliotheksfunktionen PNG_to_BMP.dll befindet sich in dem Archiv DLL_Sources.zip.

Diese Funktion liefert auch einige Beispiele für die Arbeit mit Datenreihen und Dateien unter Verwendung der Standardbibliothek. Die Methoden der Klasse CString ermöglichen die Ausführung derselben Vorgänge wie String Functions. Die Klasse CFile ist der Ursprung der Klassen CFileBin und CFileTxt. Mit ihrer Hilfe können wir das Lesen und Aufzeichnen binärer Dateien bzw. von Textdateien ausführen. Die Methoden ähneln den Funktionen zur Arbeit mit Dateien.

Abschließend beschreiben wir die Funktion CreateGoogleRequest(), sie löst Anforderungen von Kontodaten aus:

//+------------------------------------------------------------------+
///Function for creating a request for the Google Charts server
//+------------------------------------------------------------------+
string Board::CreateGoogleRequest(int X_size,int Y_size,bool type)
  {
   if(X_size>1000) X_size=1000; //check the chart size
   if(Y_size>1000) Y_size=300;  //to make sure it is not too large
   if(X_size<1) X_size=1;       //and small//s18>
   if(Y_size<1) Y_size=1;
   if(X_size*Y_size>300000) {X_size=1000; Y_size=300;}//and fit the area
   CString res; //string with results
   if(type) //create request for the balance chart
     {
      //prepare the request
      res.Assign("http://chart.apis.google.com/chart?cht=lc&chs=");
      res.Append(IntegerToString(X_size));
      res.Append("x");
      res.Append(IntegerToString(Y_size));
      res.Append("&chd=t:");
      for(int i=0;i<ChartData.Total();i++)
         res.Append(DoubleToString(ChartData.At(i),2)+",");
      res.TrimRight(",");
      //sort array
      ChartData.Sort();
      res.Append("&chxt=x,r&chxr=0,0,");
      res.Append(IntegerToString(ChartData.Total()));
      res.Append("|1,");
      res.Append(DoubleToString(ChartData.At(0),2)+",");
      res.Append(DoubleToString(ChartData.At(ChartData.Total()-1),2));
      res.Append("&chg=10,10&chds=");
      res.Append(DoubleToString(ChartData.At(0),2)+",");
      res.Append(DoubleToString(ChartData.At(ChartData.Total()-1),2));
     }
   else //create request for the pie chart
     {
      //prepare the request
      res.Assign("http://chart.apis.google.com/chart?cht=p3&chs=");
      res.Append(IntegerToString(X_size));
      res.Append("x");
      res.Append(IntegerToString(Y_size));
      res.Append("&chd=t:");
      for(int i=0;i<pie_data.Total();i++)
         res.Append(IntegerToString(pie_data.Get_val(i))+",");
      res.TrimRight(",");
      res.Append("&chdl=");
      for(int i=0;i<pie_data.Total();i++)
         res.Append(pie_data.Get_symb(i)+"|");
      res.TrimRight("|");
      res.Append("&chco=");
      int cnt=0;
      for(int i=0;i<pie_data.Total();i++)
        {
         if(cnt>11) cnt=0;
         res.Append(colors[cnt]+"|");
         cnt++;
        }
      res.TrimRight("|");
     }
   return res.Str(); //return the result
  }

Beachten Sie, dass Abrufe der Kontenverlaufskurve und des Kreisdiagramms getrennt erfasst werden. Die Methode Append() fügt hinter der vorhandenen Zeile eine weitere an, während die Methode TrimRight() Ihnen ermöglicht, am Ende der Zeile angezeigte zusätzliche Zeichen zu entfernen.


6. Abschließende Anordnung und Prüfung

Die Klasse ist fertig, probieren wir sie aus. Fangen wir mit dem Indikator OnInit() an:

Board *tablo;   //pointer to the board object
int prev_x_size=0,prev_y_size=0,prev_deals=0;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   //set indicator short name
   IndicatorSetString(INDICATOR_SHORTNAME,"IT");
   //launch the timer
   EventSetTimer(1); 
   //create object instance
   tablo=new Board;
   //and the interface
   tablo.CreateInterface(); 
   prev_deals=HistoryDealsTotal(); //number of deals
   //current sizes of the window
   prev_x_size=ChartGetInteger(0,CHART_WIDTH_IN_PIXELS); 
   prev_y_size=ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
//---
   return(0);
  }

Hier legen wir dynamisch die Klasseninstanz der Anzeigetafel an, setzen die Uhr in Gang und stellen die Hilfsvariablen ein. 

Dann platzieren wir sofort die Funktion OnDeinit(), in der wir das Objekt entfernen (was automatisch den Destruktor auf den Plan ruft) und halten die Uhr an:

void OnDeinit(const int reason)
{
   EventKillTimer(); //stop the timer
   delete table;    //and board
}

Die Funktion OnCalculate() überwacht den Strom neuer Abschlüsse bei jeder Kursschwankung (Tick-by-Tick) und aktualisiert die Anzeige, wenn sich etwas tut:

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   //prepare the history
   HistorySelect(0,TimeCurrent());
   int deals=HistoryDealsTotal();
   //update the board if number of deals has changed
   if(deals!=prev_deals) tablo.Refresh();
   prev_deals=deals;
//--- return value of prev_calculated for next call
   return(rates_total);
  }

Die Funktion OnTimer() überwacht die Veränderungen der Fenstergröße und passt gegebenenfalls die Größe der Anzeige an, zudem überwacht sie die Geschäftsvorgänge in etwa so wie OnCalculate(), falls es seltener als einmal je Sekunde zu einer Kursänderung kommt. 

void OnTimer()
  {
   int x_size=ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   int y_size=ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   //update the board if window size has changed
   if(x_size!=prev_x_size || y_size!=prev_y_size) tablo.Refresh();
   prev_x_size=x_size;
   prev_y_size=y_size;
   //update the board if number of deals has changed
   HistorySelect(0,TimeCurrent());
   int deals=HistoryDealsTotal();
   if(deals!=prev_deals) tablo.Refresh();
   prev_deals=deals;
  }

Erstellen und ausführen des Indikators:

Abbildung 3. Abschließende Ansicht der Anzeigetafel

Abbildung 3. Abschließende Ansicht der Anzeigetafel

Fazit

Liebe Leserinnen und Leser, ich hoffe, Sie konnten bei der Lektüre dieses Beitrags neue Erkenntnisse gewinnen. Ich habe versucht, Ihnen all die Möglichkeiten dieses wunderbaren Werkzeugs, das die Standardbibliothek darstellt, zu offenbaren, weil es Bequemlichkeit, Schnelligkeit und eine hohe Leistungsqualität bietet. Natürlich benötigen Sie gewisse Kenntnisse in OOP.

Viel Erfolg. 

Um anfangen zu können, müssen Sie das Archiv MQL5.rar in den entsprechenden Ordner auf Ihrem Ausgabegerät entpacken und die Verwendung von DLL zulassen. Das Archiv DLL_Sources.zip enthält den Quellcode der von mir in einer Borland C++ Builder-Umgebung mit installierter GDI programmierten Bibliotheken String_Metrics.dll und PNG_to_BMP.dll.