
Erstellen einer Anzeigetafel unter Verwendung der Klassen aus der Standardbibliothek und Google Chart API
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:
- der Basisklasse CObject
- Datenklassen
- Klassen für grafische Objekte
- einer Klasse zur Arbeit mit Diagrammen
- Klassen für die Arbeit mit Dateien
- einer Klasse für die Arbeit mit Zeichenfolgen
- Klassen für die Arbeit mit Indikatoren und Zeitreihen
- Handelsklassen
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
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
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.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/102





- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.