Criar um quadro de informação utilizando classes de biblioteca padrão e o Google Chart API

Евгений | 26 dezembro, 2013

Introdução

Para tornar a vida mais fácil para os programadores da linguagem MQL5, os desenvolvedores criaram uma biblioteca padrão, que cobre quase todas as funções API do MQL5, e torna trabalhar com elas muito mais fácil e conveniente. Esse artigo tentará criar um quadro de informações, com um número máximo de classes utilizadas pela biblioteca padrão.


1. Visão geral das classes de biblioteca padrão

Então o que exatamente é essa biblioteca? A seção de documentação da página da internet afirma que ela é composta de:

Os arquivos, contendo os códigos de todas as classes, estão localizados na pasta MQL5/Include. Quando visualizando o código da biblioteca, você notará, que ele fornece apenas as classes, mas não as funções. Consequentemente, para usá-lo, você precisa possuir algum conhecimento de programação orientada a objetos (OOP).

Todas as classes da biblioteca (exceto as de negociação) derivam da classe básica CObject. Para mostrar isso, nós vamos tentar construir um diagrama de classe, uma vez que possuímos tudo que isso necessita - a classe base e suas herdeiras. Uma vez que a linguagem do MQL5 é basicamente um subconjunto de C++, vamos utilizar o instrumento Rational Rose da IBM, que fornece ferramentas para engenharia reversa de projetos C++, para a construção automática do diagrama.

Figura 1. Diagrama das classes de biblioteca padrão

Nós não mostraremos as propriedades e métodos da classe, devido aos diagramas pesados que iríamos obter. Nós também vamos omitir agregações, uma vez que elas não são de importância para nós. Como resultado, ficamos apenas com generalizações (heranças), que nos permitem descobrir quais propriedades e métodos as classes obtém.

Como pode ser visto no diagrama, cada biblioteca que funciona com linhas, arquivos, gráficos, objetos gráficos, e arranjos, possui sua própria classe base (CString, CFile, CChart, CChartObject e CArray, respectivamente), herdada de CObject. A classe base para trabalhar com indicadores CIndicator e sua classe auxiliar CIndicators são herdadas de CArrayObj, enquanto o acesso à classe de amortecimento indicadora CIndicatorBuffer é herdada de CArrayDouble.

A cor vermelha no diagrama marca o não existente nas classes atuais, indicadores, arranjos e ChartObjects - eles são conjuntos, que incluem classes para trabalhar com indicadores, arranjos e objetos gráficos. Uma vez que há um grande número deles, e eles são herdados de um único genitor, permiti um pouco de simplificação, de forma a não desordenar o diagrama. Por exemplo, o indicador inclui CiDEMA, CiStdDev, etc.

Também vale notar que o diagrama de classe pode ser construído utilizando a criação automática do sistema de documentação Doxygen. É de alguma forma mais fácil fazer isso nesse sistema do que no Rational Rose. Mais sobre o Doxygen pode ser encontrado no artigo Documentação gerada automaticamente para código MQL5.


2. O problema

Vamos tentar criar uma tabela de informação com o número máximo de classes da biblioteca padrão.

O que o quadro irá exibir? Algo como um relatório detalhado do MetaTrader 5, isto é:

Figura 2. A aparência do relatório detalhado

Figura 2. A aparência do relatório detalhado

Como podemos ver, o relatório exibe um gráfico de balanço e algumas figuras de negociação. Mais informações sobre os métodos para calcular esses indicadores podem ser encontradas no artigo O que os números no relatório de testes do especialista significam.

Já que o quadro é utilizado puramente com fins informativos, e não realiza nenhuma operação de negociação, será melhor implementá-lo como um indicador, em uma janela separada, de forma a evitar fechar o gráfico verdadeiro. Além disso, colocá-lo em uma sub janela permite fácil escala, e mesmo fechando o quadro com um único movimento do seu mouse.

Você pode também querer complementar o relatório com um gráfico pizza, que exibirá o número de transações realizadas no instrumento, relativas ao número de transações total.


3. Projetando a interface

Definimos os nossos objetivos - nós precisamos de um relatório detalhado da sub janela do gráfico principal.

Implementamos o nosso quadro de informações como uma classe: Vamos começar:

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

Os dados de classe protegida são os elementos de interface e dados de gráficos de acordo, balanço e pizza (a classe PieData será discutida abaixo). Indicadores de negociação e alguns métodos são privados. Eles são privados porque o usuário não deve ter acesso direto a eles, eles são calculados dentro da classe, e eles podem ser contados apenas através de chamar o método público apropriado.

Também os métodos da criação da interface e o cálculo dos indicadores são privados, uma vez que aqui você precisa suportar uma sequência rigorosa de chamadas de métodos. Por exemplo, é possível calcular os indicadores sem possuir os dados para o cálculo, ou atualizar a interface, sem ter que criá-la de antemão. Assim, não permitiremos que o usuário "dê um tiro no próprio pé".

Vamos imediatamente lidar com os construtores e destruidores de uma classe, de forma que nós não tenhamos que retornar a eles depois:

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

No construtor, ligaremos um objeto do tipo CChart ao gráfico atual com a ajuda de seu método Attach(). O método Detach(), chamado no destruidor, desfará a união do gráfico e do objeto. Objeto de dados, que é um apontador para um objeto do tipo CArrayObj, recebeu o endereço do objeto, criado dinamicamente utilizando a operação nova e removido em destruidor utilizando a operação deletar. Não esqueça de verificar a presença do objeto utilizandoo CheckPointer() antes de deletar, caso contrário, um erro ocorrerá.

Mais informações sobre a classe CArrayObj serão fornecidas posteriormente. O método Shutdown() da classe CArrayDouble como qualquer outra classe é herdado da classe CArray (ver o diagrama de classes) limpará e liberará a memória, ocupada pelo objeto. O método Delete() dos herdeiros da classe CChartObject remove o objeto do gráfico.

Assim, o construtor aloca a memória e o destruidor a libera, e remove os objetos gráficos, criados pela classe.

Vamos agora lidar com a interface. Como afirmado acima, o método CreateInterface() cria uma interface do quadro:

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

Para um arranjo compacto de todos os elementos, primeiro, utilizando os métodos WidthInPixels() e GetInteger() da classe CChart, descubra o comprimento e largura da subjanela do indicador, na qual o quadro estará localizado. Então criaremos células, que incluirão os valores dos indicadores, utilizando o método Create() da classe CChartObjectEdit (cria o "campo de entrada"), todos os herdeiros possuem esse método de CChartObject.

Observe como é conveniente utilizar a biblioteca padrão para operações desse tipo. Sem ela nós teríamos que criar cada objeto, utilizando a função ObjectCreate, e configurar as propriedades dos objetos, utilizando tais funções como ObjectSet, que levariam a redundância no código. E quando depois quiséssemos mudar as propriedades dos objetos, seria necessário controlar cuidadosamente os nomes dos objetos de forma a evitar confusão. Agora podemos simplesmente criar um arranjo de objetos gráficos, e olhar através dele conforme desejarmos.

Adicionalmente, podemos conseguir/configurar propriedades de objetos utilizando uma função, se forem criadores sobrecarregados da classe, como o método Color() da classe CChartObject. Quando chamado com os parâmetros ele configura temas, sem os parâmetros - ele retorna a cor do objeto. Posicionar o gráfico pizza próximo ao gráfico de balanço, ele tomará um quarto da largura total da janela.

O método de atualização() atualiza o quadro. Do que consiste essa atualização? Precisamos contar os indicadores, inseri-los nos objetos gráficos, e reescalar o quadro, se o tamanho da janela na qual ele está localizado tiver sido alterado. O quadro deve tomar todo o espaço livre da janela.

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

Há muitos códigos, análogos ao método CreateInterface(), primeiro a função Calculate() calcula os indicadores, então eles são inseridos nos objetos gráficos, e simultaneamente os tamanhos dos objetos são sintonizados aos tamanhos das janelas, utilizando os métodos X_Size() e Y_Size(). Os métodos X_Distance e Y_Distance mudam a posição do objeto.

Preste atenção à função GetFontSize(), ela seleciona um tamanho de fonte, que não fará o texto "extravasar" as bordas do recipiente após ser reescalado, e, reciprocamente, não se tornará muito pequeno.

Vamos considerar essa função mais de perto:

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

A função GetStringMetrics() é importada do DLL, descrito acima, o código do qual pode ser encontrado no arquivo DLL_Sources.zip e pode ser modificado se necessário. Acho que pode ser útil se você escolher projetar a sua própria interface no projeto.

Nós terminamos com a interface do usuário, vamos nos voltar para o cálculo dos indicadores de negociação.


4. Cálculo dos indicadores de negociação

O método Calculate() realiza os cálculos.

Mas também precisamos do método GetData(), que recebe os dados necessários:

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

Primeiro, vamos considerar o método de armazenamento de dados. A biblioteca padrão fornece as classes de estruturas de dados, que permitem a você evitar de utilizar arranjos. Nós precisamos de um arranjo bidimensional, no qual armazenaremos dados sobre lucros e os tipos de transações no histórico. Mas a biblioteca padrão não fornece classes explicitas para organizar arranjos bidimensionais, no entanto existem as classes CArrayDouble (arranjo de dados do tipo duplo) e CArrayObj (arranjo dinâmico de apontadores para casos da classe CObject e seus herdeiros). Isto é, podemos criar um arranjo de arranjos do tipo duplo, o que é exatamente o que é feito.

Claro, as declarações como ((CArrayDouble *) Data.At (Data.Total () - 1 )). Add (profit) não parecem tão organizadas como data [i] [j] = profit, porém, isso é apenas a primeira vista. Afinal, ao simplesmente declarar um arranjo, sem utilizar as classes da biblioteca padrão, somos privamos de tais benefícios como o gerenciador de memória integrada, a habilidade de inserir um arranjo diferente, comparar arranjos, encontrar itens, etc. Assim, o uso das classes de organização de memória nos liberta da necessidade de controlar o transbordamento do arranjo, e nos fornece vários instrumentos úteis.

O método Total() da classe CArray (ver figura 1) retorna o número de elementos no arranjo, o método Add() os adiciona, o método At() retorna os elementos.

Uma vez que decidimos construir um gráfico pizza, de forma a exibir o número de acordos para símbolos, nós precisamos dos dados necessários.

Iremos escrever uma classe auxiliar, concebida para coletar esses dados:

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

Não é sempre que as classes da biblioteca padrão serão capazes de nos fornecer com os métodos necessários para o trabalho. Nesse exemplo, o método Search() da classe CArrayString não é adequado, porque de forma a aplicá-lo, precisamos primeiro separar o arranjo, que viola a estrutura dos dados. Portanto, tivemos que escrever o nosso próprio método.

O cálculo de características de negociação é implementado no método 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_lossesmax_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_lossesmax_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. Utilizando o Google Chart API para criar um gráfico de balanço

Google Chart API permite que os desenvolvedores criem diagramas instantaneamente de vários tipos. O Google Chart API é armazenado no link para o recurso (URL) nos servidores da internet do Google e quando recebendo um link formatado corretamente (URL), returna o diagrama para uma imagem.

As características do diagrama (cores, cabeçalhos, eixo, pontos no gráfico, etc) são especificadas pelo link (URL). A imagem resultante pode ser armazenada em um sistema de arquivos ou base de dados. O aspecto mais agradável é que o Google Chart API é de graça e não exige possuir uma conta ou passar por um processo de registro.

O método GetChart() recebe o gráfico do Google e o salva no 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()");
  }
  

Você pode conseguir detalhes para trabalhar com ferramentas online do API Windows e MQL5 do artigo Utilizando WinInet.dll para troca de dados entre terminais via a internet. Portanto, não vamos gastar tempo com nisso. A função importada Convert_PNG() foi escrita por mim para converter imagens PNG para BMP.

É necessário porque o Google Chart devolve gráficos no formato PNG ou GIF, e o objeto "etiqueta do gráfico" somente aceita imagens BMP. O código das funções de biblioteca PNG_to_BMP.dll correspondentes pode ser encontrado no arquivo DLL_Sources.zip.

Essa função também mostra alguns exemplos de trabalhar com linhas e arquivos, utilizando a biblioteca padrão. Os métodos da classe CString permitem o desempenho da mesma operação que funções em cadeia. A classe CFile é a base para as classes CFileBin e CFileTxt. Com a ajuda deles, nós podemos produzir a leitura e o registro dos arquivos binários e de texto, respectivamente. Os métodos são similares a funções para trabalhar com arquivos.

Finalmente, descreveremos a função CreateGoogleRequest() - ela cria consultas a partir dos dados no balanço:

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

Observe que pedidos para o o gráfico de balanço e gráfico de pizza são coletados separadamente. O método Append() adiciona outra fileira ao final da fileira existente, e o método TrimRight() permite a você remover caracteres extra, exibidos no fim da linha.


6. Montagem final e verificação

A classe está pronta, vamos testá-la. Nós começamos com o indicador 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);
  }

Aqui, dinamicamente criamos a instância de classe Board, acionamos o temporizador, iniciamos as variáveis auxiliares.

Imediatamente, posicionamos a função OnDeinit(), removeremos o objeto (que automaticamente invoca o destrutor) e para o temporizador:

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

A função OnCalculate() irá monitorar o fluxo de novos negócios, tick por tick, e atualizar a tela, se isso ocorrer:

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

A função OnTimer() monitora as mudanças no tamanho da janela, e, se necessário, customiza o tamanho da tela, ela também monitora os acordos como OnCalculate(), em caso de as variações forem mais raras do que 1 por segundo.

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

Compile o e execute o indicador:

Figura 3. A visualização final da tabela

Figura 3. A visualização final da tabela

Conclusão

Querido leitor, espero que na leitura desse artigo, você encontre algo novo para você. Eu tentei abrir perante você todas as potencialidades de um instrumento tão maravilhoso como a biblioteca padrão, porque ela fornece conveniência, velocidade e alta qualidade de desempenho. Claro, você precisa possuir algum conhecimento de OOP.

Boa sorte.

Para começar, descompacte o arquivo MQL5.rar dentro da pasta terminal, e permita o uso do DLL. O arquivo DLL_Sources.zip contém os códigos-fonte das bibliotecas String_Metrics.dll PNG_to_BMP.dll, eles foram escritos por mim no ambiente Borland C++ Builder com um GDI instalado.