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:
- classe básica CObject
- Classes dos dados
- Classes para objetos gráficos
- Classes para trabalhar com gráfico
- Classes para operações com arquivos
- Classes para operações de cadeia
- Classes para trabalhar com indicadores e série temporal
- Classes de negociação
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
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
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.