Creazione di una Information Board utilizzando le Standard Library Classes e Google Chart API

Евгений | 9 dicembre, 2021


Introduzione

Per rendere la vita più facile ai programmatori del linguaggio MQL5, i designer hanno creato una Standard Library che copre quasi tutte le funzioni API MQL5 e rende il lavoro molto più semplice e conveniente. In questo articolo, si cerca di creare una information board con un numero massimo di classi utilizzate dalla libreria standard.


1. Panoramica delle classi delle Standard Library

Quindi, cos'è esattamente questa libreria? La sezione Documentazione del sito web afferma che è composta da:

I file, contenenti i codici di tutte le classi, si trovano nella cartella MQL5/Include. Quando si visualizza il codice della libreria, noterai che fornisce solo le classi, ma non le funzioni. Di conseguenza, per usarlo, è necessario avere una certa conoscenza della programmazione orientata agli oggetti (OOP). 

Tutte le classi di librerie (ad eccezione di quelle di trading) derivano dalla classe CObject di base. Per mostrarlo, proveremo a costruire un Class diagram poiché abbiamo tutto ciò che questo richiede: la classe di base e i suoi derivati. Poiché il linguaggio MQL5 è fondamentalmente un sottoinsieme di C ++, usiamo lo strumento IBM Rational Rose, che fornisce strumenti per il reverse -engineering di progetti C ++, per la costruzione automatica del diagramma.

 

Figura 1. Diagramma delle classi di Standard Library

Non mostreremo le proprietà e i metodi della classe, a causa dei diagrammi complicati che otterremmo. Ometteremo anche le aggregazioni, poiché non hanno alcuna importanza per noi. Di conseguenza, ci rimangono solo le generalizzazioni (eredità), che ci permettono di scoprire quali proprietà e metodi ottengono le classi.

Come si può vedere dal diagramma, ogni componente della libreria che funziona con linee, file, grafici, oggetti grafici e matrici, ha una propria classe di base, (CString, CFile, CChart, CChartObject e CArray,, rispettivamente), ereditata dal CObject. La classe di base per l'utilizzo degli indicatori CIndicator e la sua classe CIndicators ausiliaria sono ereditate da CArrayObj, mentre l'accesso alla classe buffer di indicatori CIndicatorBuffer viene ereditato da CArrayDouble.

Il colore cremisi nel diagramma indica le classi inesistenti nell'attualità, gli indicatori, gli array e i ChartObjects - sono insiemi che includono classi per lavorare con indicatori, array e oggetti grafici. Poiché ce n'è un gran numero e sono ereditati da un singolo genitore, ho permesso una certa semplificazione, in modo da non ingombrare il diagramma. Ad esempio, l'indicatore include CiDEMA, CiStdDev, ecc.

Vale anche la pena notare che il diagramma delle classi può anche essere costruito utilizzando la creazione automatica del sistema di documentazione Doxygen. È un po’ più facile farlo con questo sistema piuttosto che in Rational Rose. Ulteriori informazioni su Doxygen sono disponibili nell'articolo Documentazione auto-generata per il codice MQL5.


2. Il problema

Proviamo a creare una tabella informativa con il numero massimo di classi Standard Library.

Cosa mostrerà la board? Qualcosa di simile a un rapporto dettagliato di MetaTrader 5, cioè:

Figura 2. L’aspetto del report dettagliato

Figura 2. L'aspetto del report dettagliato

Come possiamo vedere, il report mostra un grafico del bilancio e alcune cifre di trading. Ulteriori informazioni sui metodi per il calcolo di questi indicatori sono disponibili nell'articolo Cosa significano i Numeri nel Report Expert Testing.

Poiché la board viene utilizzata esclusivamente a scopo informativo e non esegue alcuna operazione di trading, sarà meglio implementarla come indicatore, in una finestra separata, al fine di evitare di chiudere il grafico effettivo. Inoltre, posizionarlo in una sotto-finestra consente un facile ridimensionamento e persino la chiusura della board con un solo movimento del mouse.

Può anche darsi il caso che vuoi completare il report con un grafico a torta che mostrerà il numero di transazioni effettuate sullo strumento, rispetto al numero totale delle transazioni.


3. Progettazione dell'interfaccia 

Abbiamo definito i nostri obiettivi; abbiamo bisogno di un report dettagliato nella sotto-finestra del grafico principale.

Implementiamo la nostra information board come classe. Iniziamo:

//+------------------------------------------------------------------+
///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(); 
  };

I dati di classe protetti sono gli elementi dell'interfaccia e i dati del grafico a torta, del bilanciamento e della torta (della la classe PieData parleremo in seguito). Gli indicatori di trading e alcuni metodi sono privati. Sono privati perché l'utente non dovrebbe avere accesso diretto ad essi, sono calcolati all'interno della classe e possono essere contati solo chiamando il relativo metodo pubblico .

Anche i metodi di creazione dell'interfaccia e il calcolo degli indicatori sono privati, poiché qui è necessario affrontare una rigorosa sequenza di chiamate di metodi. Ad esempio, è impossibile calcolare gli indicatori senza essere in possesso dei dati per il calcolo o aggiornare l'interfaccia, senza doverla creare in anticipo. Pertanto, non consentiremo all'utente di "spararsi sui piedi". 

Occupiamoci immediatamente di constructor e destructor di una classe, in modo da non dover tornare più tardi sull’argomento:

//+------------------------------------------------------------------+
///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
  }

Nel constructor, assoceremo un oggetto di tipo CChart al grafico corrente con l'aiuto del suo metodo Attach(). Il metodo Detach(), chiamato nel destructor, dissocerà il grafico dall'oggetto. Il data object, che è un puntatore a un oggetto di tipo CArrayObj , ha ricevuto l'indirizzo dell'oggetto, creato dinamicamente utilizzando la nuova operazione e rimosso nel destructor utilizzando la funzione delete. Non dimenticare di controllare la presenza dell'oggetto utilizzandoil CheckPointer() prima di eliminarlo,, altrimenti si verificherà un errore.

Ulteriori informazioni sulla classe CArrayObj verranno fornite in seguito. Il metodo Shutdown() della classe CArrayDouble, proprio come qualsiasi altra classe ereditata dalla classe CArray (si veda il diagramma delle classi), cancellerà e libererà la memoria occupata dall'oggetto. Il metodo Delete() degli eredi della classe CChartObject rimuove l'oggetto dal grafico.

Pertanto, il constructor occupa la memoria e il destructor la libera, rimuovendo gli oggetti grafici creati dalla classe. 

A questo punto, occupiamoci dell'interfaccia. Come detto sopra, il metodo CreateInterface() crea un'interfaccia della board:

//+------------------------------------------------------------------+
///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
  }

Per una disposizione omogenea di tutti gli elementi, in primo luogo, utilizzando i metodi WidthInPixels() e GetInteger() della classe CChart , scopri la lunghezza e la larghezza della sotto-finestra dell'indicatore, in cui verrà posizionata la board. Quindi, creiamo le celle che includeranno i valori degli indicatori, utilizzando il metodo Create() della classe CChartObjectEdit (crea il "campo di input"), tutti gli eredi possiedono questo metodo di CChartObject.

Si noti quanto sia conveniente utilizzare la Standard Library per operazioni di questo tipo. Senza di essa, dovremmo creare ogni oggetto usando la funzione ObjectCreate e impostare le proprietà degli oggetti, usando funzioni come ObjectSet, che porterebbe alla ridondanza del codice. E quando in seguito vorremmo cambiare le proprietà degli oggetti, sarebbe necessario controllare attentamente i nomi degli oggetti per evitare confusione. Ora, possiamo semplicemente creare degli oggetti grafici e analizzarli come desideriamo.

Inoltre, possiamo ottenere/impostare le proprietà degli oggetti usando una funzione, se si trattava di overloaded creator della classe, come il metodo Color() della classe CChartObject. Quando viene chiamato con i parametri, esso li imposta senza parametri - restituisce il colore dell'oggetto. Posiziona il grafico a torta accanto al grafico di bilanciamento; occuperà un quarto della larghezza totale dello schermo.

Refresh method() aggiorna la scheda. In cosa consiste questo aggiornamento? Dobbiamo contare gli indicatori, inserirli negli oggetti grafici e ridimensionare la board, qualora le dimensioni della finestra in cui si trova fossero state modificate . La board dovrebbe occupare tutto lo spazio libero della finestra.

//+------------------------------------------------------------------+
///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
  }

Ci sono molti codici, analoghi al metodo CreateInterface(), prima la funzione Calculate() calcola gli indicatori, poi vengono inseriti negli oggetti grafici e contemporaneamente le dimensioni degli oggetti vengono adattate in base alle dimensioni delle finestre, utilizzando i metodi X_Size() e Y_Size(). I metodi X_Distance e Y_Distance modificano la posizione dell'oggetto.

Prestare attenzione alla funzione GetFontSize(). Essa seleziona una dimensione del carattere e farà in modo che il resto non “oltrepassi” i bordi " dei bordi del contenitore dopo essere stato ridimensionato e, al contrario, non diventerà troppo piccolo.

Consideriamo più da vicino questa funzione :

//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;
  }

La funzione GetStringMetrics() viene importata dalla DLL, descritta in precedenza, il cui codice può essere trovato nell'archivio DLL_Sources.zip e può essere modificato se necessario. Penso che ciò potrebbe tornare utile se decidi di creare la tua interfaccia nel progetto.

Abbiamo terminato l'interfaccia utente, passiamo al calcolo degli indicatori di trading.


4. Calcolo degli indicatori di trading

Il metodo Calculate() esegue i calcoli.

Tuttavia, abbiamo anche bisogno del metodo GetData() che riceve i dati necessari:

//+------------------------------------------------------------------+
///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
     }
  }

Innanzitutto, consideriamo il metodo di archiviazione dei dati. La standard library fornisce le classi di strutture di dati che ci permettono di non utilizzare gli array. Abbiamo bisogno di un array bidimensionale in cui memorizzeremo i dati sui profitti e sui tipi di transazioni nella cronologia. Tuttavia, la standard library non fornisce classi esplicite per l'organizzazione degli array bidimensionali, ma ci sono classi CArrayDouble (array di doppio tipo di dati) e CArrayObj (array dinamico di puntatori alle istanze della classe  CObject e ai suoi eredi). Ad esempio, possiamo creare array di tipo doppio, che è esattamente ciò che viene fatto. 

Naturalmente, le affermazioni come ((CArrayDouble *) Data.At (Data.Total () - 1 )). Add (profit) non sembrano pulite come i dati [i] [j] = profit , ma questo avviene solo ad un primo impatto. Dopotutto, semplicemente dichiarando un array, senza utilizzare le classi di standard library, siamo privati dei vantaggi di un gestore di memoria integrato: la possibilità di inserire un array diverso, confrontare array, trovare elementi, ecc. Pertanto, l'uso di classi di organizzazione della memoria ci libera dalla necessità di controllare l'overflow dell'array e ci fornisce molti strumenti utili. 

Il metodo Total() della classe CArray (si veda la Fig. 1.) restituisce il numero di elementi nell’array, il metodo Add() li aggiunge, il metodo At() restituisce gli elementi.

Dal momento che abbiamo deciso di costruire un grafico a torta per visualizzare il numero di offerte per i simboli, abbiamo bisogno di raccogliere i dati necessari.

Scriveremo una classe ausiliaria, destinata alla raccolta di questi dati:

//+------------------------------------------------------------------+
///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);}
  };

Le classi di standard library non sempre saranno in grado di fornirci i metodi necessari per il lavoro. In questo esempio, il metodo Search() della classe CArrayString non è adatto, perché per applicarlo, dobbiamo prima ordinare l’array che viola la struttura dei dati. Quindi, abbiamo dovuto scrivere il nostro metodo.

Il calcolo delle caratteristiche di trading viene implementato nel metodo Calculate():

//+------------------------------------------------------------------+
///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. Utilizzo di Google Chart API per creare un grafico di bilancio

Google Chart API consente agli sviluppatori di creare istantaneamente diagrammi di vario tipo. Google Chart API viene memorizzata nel link alla risorsa (URL) sui web server di Google e, quando si riceve un link formattato correttamente (URL), restituisce il diagramma sotto forma di immagine.

Le caratteristiche del diagramma (colori, intestazioni, assi, punti sul grafico, ecc.) sono specificate dal collegamento (URL). L'immagine risultante può essere memorizzata in un file system o in un database. La cosa più piacevole è che Google Chart API è gratuito e non richiede di avere un account o di passare attraverso un processo di registrazione. 

Il metodo GetChart() riceve il grafico da Google e lo salva sul disco:

#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()");
  }
  

È possibile ottenere i dettagli per lavorare con gli strumenti online di API Windows e MQL5 dall'articolo Utilizzo di WinInet.dll per lo scambio di dati tra terminali via Internet. Quindi, non dedicherò tempo a questo. La funzione importata Convert_PNG() è stata scritta da me per convertire le immagini PNG in BMP.

Ciò è necessario perché Google Chart restituisce grafici in formato PNG o GIF e l'oggetto "etichetta grafica" accetta solo immagini BMP. Il codice delle funzioni della libreria PNG_to_BMP.dll corrispondente può essere trovato nell’archivio DLL_Sources.zip.

Questa funzione mostra anche alcuni esempi di lavoro con linee e file, utilizzando la standard library. I metodi della classe CString consentono di esguire le stesse operazioni Funzioni di Stringa . La classe CFile è la base per le classi CFileBin e CFileTxt. Con il loro aiuto, possiamo produrre, rispettivamente, la lettura e la registrazione di file binari e di testo. I metodi sono simili alle funzioni per lavorarecon i file.

Infine, descriveremo la funzione CreateGoogleRequest () - essa crea delle query dai dati sul bilancio:

//+------------------------------------------------------------------+
///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
  }

Si noti che le richieste per il grafico di bilancio e il grafico a torta vengono raccolte separatamente. Il metodo Append() aggiunge un'altra riga alla fine della riga esistente e il metodo TrimRight() consente di rimuovere caratteri aggiuntivi, visualizzati alla fine della riga.


6. Assemblaggio e test finale

La classe è pronta, testiamola. Iniziamo con l'indicatore OnInit ():

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);
  }

Qui creiamo dinamicamente l'istanza della classe Board, avviamo il timer e inizializziamo le variabili ausiliarie. 

Subito dopo, inseriamo la funzione OnDeinit(), lì rimuoveremo l'oggetto (che richiama automaticamente il destructor) e fermeremo il timer:

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

Lafunzione OnCalculate() monitorerà il flusso di nuove offerte, tick per tick e aggiornerà la visualizzazione, se si verifica quanto segue:

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);
  }

La funzioneOnTimer() monitora le variazioni delle dimensioni della finestra e, se necessario, personalizza le dimensioni del display, monitora anche le offerte proprio come OnCalculate(), nel caso in cui i tick arrivano meno frequentemente che 1 al secondo. 

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;
  }

Compilare ed eseguire l'indicatore:

Figura 3. Vista finale della tabella

Figura 3. Vista finale della tabella

Conclusione

Caro lettore, spero che leggendo questo articolo, troverai qualcosa di nuovo utile per te stesso. Ho cercato di illustrarti tutte le potenzialità di uno strumento meraviglioso come la Standard Library, perché offre praticità, velocità e alta qualità delle prestazioni. Naturalmente, è necessario avere una certa conoscenza dell’OOP.

Buona fortuna! 

Per iniziare, decomprimere l'archivio MQL5.rar nella cartella del terminale e consentire l'uso della DLL. L'archivio DLL_Sources.zip contiene i codici sorgente delle librerie String_Metrics.dll PNG_to_BMP.dll; essi sono stati scritti da me in ambiente Borland C++ Builder con un GDI installato.