Creación de un panel de información mediante las clases de la Librería estándar y Google Chart API

Евгений | 18 diciembre, 2013


Introducción

Para hacer la vida más fácil a los programadores del lenguaje MQL5, los diseñadores han creado una Librería estándar, que cubre la mayoría de las funciones API MQL5, y hace que su utilización sea mucho más fácil y oportuna. En este artículo trataremos de crear un panel de información, con el máximo número de clases que utiliza la Librería estándar.


1. Resumen de las clases de la Librería estándar

¿Por tanto, qué es exactamente esta Librería? La sección de Documentación de los estados de los sitios web está formada por:

Los archivos que contienen todo tipo de clases, están ubicados en la carpeta MQL5/Include. Al ver el código de la Librería, se dará cuenta de que proporciona únicamente las clases, pero no las funciones. En consecuencia, para usarlas, hay que tener algunos conocimientos de programación orientada a objetos (POO). 

Todas las clases de la Librería (excepto las del trading) proceden de la clase básica CObject. Para mostrarla, intentaremos construir un Diagrama de clase, ya que disponemos de todo lo necesario para ello; la clase base y las que y sus derivadas. Puesto que el lenguaje MQL5 es básicamente un subconjunto de C++, vamos a usar el instrumento IBM Rational Rose, que proporciona las herramientas para la ingeniería inversa de los proyectos de C++, con el fin de llevar a cabo la construcción automática del diagrama.

 

Figura 1. Diagrama de las clases de la Librería estándar

No vamos a mostrar las propiedades y métodos de las clases, ya que los diagramas que vamos a obtener serían muy voluminosos. Tampoco vamos a ver las agregaciones, ya que no nos importan. Como resultado, nos quedamos sólo con generalizaciones (herencias), que nos permiten averiguar cuáles son las propiedades y métodos de adquisición de las clases.

Como puede verse en el diagrama, todos los componentes de la Librería que trabajan con cadenas, archivos, gráficos, objetos gráficos, y matrices, tienen su propia clase base (CString, CFile, CChart, CChartObject y CArray, respectivamente), heredadas de CObject. La clase base para trabajar con indicadores CIndicator y su clase adicional CIndicators son heredadas de CArrayObj, mientras que la clase para el acceso al buffer del indicador CIndicatorBuffer es heredada de CArrayDouble.

El color carmesí del diagrama indica que en estas clases no existen indicators ni arrays ni ChartObjects; que son conjuntos que incluyen clases para indicadores, matrices y objetos gráficos. Puesto que hay un gran número y se heredan de un único origen, me he permitido realizar alguna simplificación, para no saturar el diagrama. Por ejemplo, el indicador incluye CiDEMA, CiStdDev, etc.

Además, vale la pena destacar que se puede también realizar el diagrama de la clase mediante el sistema de creación automática de documentación Doxygen. Es un poco más fácil hacerlo con este sistema que con el Rational Rose. Se puede encontrar más información acerca de Doxygen en el artículo Generación automática de documentación para el código MQL5.


2. Descripción del problema

Vamos a tratar de crear un panel de informaciones con el máximo número de clases de la Librería estándar.

¿Qué aparecerá en el panel? Algo parecido a un informe detallado de MetaTrader 5, es decir:

Figura 2. Captura de un informe detallado

Figura 2. Captura de un informe detallado

Como podemos observar, el informe muestra un diagrama de balance y algunos datos de trading. Se puede encontrar más información acerca de los métodos de cálculo de estos indicadores en el artículo Lo que indican de los números en el informe de la prueba del Experto.

Puesto que el panel tiene un carácter meramente informativo, y no realiza ninguna transacción, sería mejor implementarlo como indicador en una ventana separada, para evitar el cierre del gráfico actual. Además, poner el panel en una ventana secundaria permite ajustarla más fácilmente, e incluso con un sólo movimiento del ratón.

No estaría de más añadir al informe una gráfica circular, que mostrará la cantidad de transacciones realizadas en el instrumento relativa al número total de transacciones.


3. Diseño de la interfaz 

Ya están definidos los objetivos; necesitamos un informe detallado en una subventana del gráfico principal.

Implementamos nuestro panel de informaciones como una clase. Empecemos:

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

Los datos protegidos de la clase son los elementos y transacciones de la interfaz, el balance, y los datos del gráfico circular (más adelante hablaremos de la clase PieData). Los indicadores de trading y algunos métodos son privados. Son privados porque el usuario no debe tener acceso directo a ellos, se calculan en la clase, y solo se pueden calcular mediante la llamada al método público adecuado. 

Los métodos de creación de la interfaz y del cálculo de los indicadores son también privados, puesto que se debe seguir una rigurosa secuencia de métodos de llamadas. Por ejemplo, es imposible calcular los indicadores sin disponer de datos para el cálculo, o actualizar la interfaz sin haberla creado antes. Por lo tanto, no vamos a permitir al usuario "pegarse un tiro". 

En seguida vamos a abordar los constructores y destructores de clases, y no tenemos que volver a verlos más adelante:

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

En el constructor, uniremos un objeto del tipo CChart al gráfico actual con la ayuda de su método Attach(). El método Detach(), al que se llama en el destructor, separará el gráfico del objeto. El objeto Data, es un puntero a un objeto de tipo CArrayObj, recibirá la dirección del objeto, que se crea de manera dinámica mediante la operación new y se borra en el destructor mediante la operación delete. No hay que olvidarse de comprobar la presencia de datos usando el CheckPointer() antes de borrar, de lo contrario, se producirá un error.

Se proporcionará más información acerca de la clase CArrayObj más adelante. El método Shutdown() de la clase CArrayDouble como cualquier otra clase es heredado de la clase CArray (ver el diagrama de clases) borrará y liberará la memoria ocupada por el objeto. El método Delete() heredado de la clase CChartObject elimina el objeto del gráfico.

Por lo tanto, el constructor asigna la memoria y el destructor la libera y elimina los objetos gráficos creados por la clase. 

Vamos a ver ahora la interfaz. Como se ha indicado anteriormente, el método CreateInterface() crea la interfaz del panel:

//+------------------------------------------------------------------+
///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 una disposición compacta de todos los elementos, primero, mediante los métodos WidthInPixels() y GetInteger() de la clase CChart, averiguamos la altura y el ancho de la subventana del indicador dónde se ubicará el panel. A continuación, creamos las celdas que van a contener los valores de los indicadores, mediante el método Create() de la clase CChartObjectEdit (crea el "campo de entrada"; input field), todas las clases heredadas tienen este método de CChartObject.

Cabe destacar lo cómodo que es usar la Librería estándar para operaciones de este tipo. Sin ella hubiéramos tenido que crear cada objeto, mediante la función PbjectCreate, y definir las propiedades de los objetos, mediante funciones como ObjectSet, lo que llevaría a la redundancia del código. Y si más adelante queremos modificar las propiedades de los objetos, habrá que comprobar cuidadosamente los nombres de los objetos para evitar confusiones. Ahora podemos sencillamente crear una matriz de objetos, y consultarla como queramos.

Además, podemos obtener o definir las propiedades de los objetos de una función, si se sobrecarga mediante los creadores de la clase, como el método Color() de la clase CChartObject. Si se le llama con los parámetros, los establece, y sin parámetros devuelve el color del objeto. Además, coloca el gráfico circular cerca del gráfico de balance, ocupando la cuarta parte del ancho total de la pantalla.

La actualización de method() actualiza el panel. ¿En qué consiste esta actualización? Tenemos que calcular los indicadores, agregarlos a los objetos gráficos y redimensionar el panel, en el caso de que haya cambiado el tamaño de su ventana. El panel debe ocupar todo el espacio libre de la ventana.

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

Hay muchos códigos, parecidos al método CreateInterface(), primero la función Calculate() calcula los indicadores, después se introducen en los objetos gráficos, y al mismo tiempo se adaptan los tamaños de los objetos a los tamaños de las ventanas, mediante los métodos X_Size() y Y_Size(). Los métodos X_Distance e Y_Distance cambian la posición del objeto.

Preste atención a la función GetFontSize() que selecciona el tamaño de la fuente, de modo que no haya "desbordamiento" del texto fuera de su área después de cambiar su dimensión y, por otro lado, que no se haga demasiado pequeño.

Echemos un vistazo a esta función de cerca:

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

Se ha importado la función GetStringMetrics() desde la DLL, se puede encontrar su código descrito más arriba en el archivo DLL_Sources.zip y se puede modificar si es necesario. Creo que puede resultar muy útil si decide diseñar su propia interfaz en el proyecto.

Hemos finalizado la interfaz de usuario, vamos a volver al cálculo de los indicadores de trading.


4. Cálculo de los indicadores de trading

El método Calculate() realiza los cálculos.

Pero necesitamos también el método GetData(), que recibe los datos necesarios:

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

Primero, vamos a ver el método de almacenamiento de datos. La Librería estándar proporciona las clases de estructuras de datos, que le permiten prescindir de las matrices. Necesitamos una matriz de dos dimensiones, en la cual almacenaremos los datos de los beneficios y el tipo de transacciones del historial. Pero la Librería estándar no proporciona clases explícitas para la organización de matrices de dos dimensiones, sin embargo, están las clases CArrayDouble (matriz de datos de tipo doble) y CArrayObj (matriz dinámica de punteros a las instancias de la clase CObject y sus herederas). Es decir, podemos crear una matriz de matrices de tipo doble, y esto es exactamente lo que se hace. 

Desde luego, los declaraciones tipo ((CArrayDouble *) Data.At (Data.Total () - 1 )).Add (profit) no se ven tan claras como data [i] [j] = profit, pero sólo a primera vista. Después de todo, con una simple declaración de una matriz y si no usamos las clases de la Librería estándar, no tendremos algunas ventajas, como el administrador integrado de memoria, la capacidad de insertar una matriz diferente, comparar matrices, buscar elementos, etc. Por lo tanto, el uso de las clases de organización de memoria nos libera de la necesidad de controlar el desbordamiento de la matriz, y nos proporciona muchos instrumentos útiles. 

El método Total() de la clase CArray (ver Fig. 1.) devuelve el número de elementos en la matriz, el método Add() los añade y el método At() devuelve los elementos.

Puesto que hemos decidido crear un gráfico circular, vamos a necesitar recoger los datos necesarios para poder mostrar el número de transacciones de símbolos.

Vamos a escribir una clase adicional, destinada a recoger estos datos:

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

Las clases de la Librería estándar no son siempre capaces de proporcionarnos los métodos necesarios para trabajar. En este ejemplo, el método Search() de la clase CArrayString no es adecuado, ya que para poder aplicarlo, primero tenemos que ordenar la matriz, cosa que viola la estructura de los datos. Por lo tanto, tenemos que escribir nuestro propio método.

Se implementa el cálculo de los datos de la operación de trading en el 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_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. El uso de Google Chart API para crear un gráfico de balance

Google Chart API permite a los desarrolladores crear diagramas de distintos tipos de forma inmediata. Se almacena Google Chart API en el enlace hacia el recurso (URL) en los servidores Web de Google y cuando recibe un enlace (URL) con el formato correcto, devuelve el diagrama en forma de imagen.

Las características del diagrama (colores, encabezados, ejes, puntos del gráfico, etc.) se especifican por el enlace (URL). Se puede almacenar la imagen resultante en un sistema de archivos o en una base de datos. Lo mejor de todo es que Google Chart API es gratuita y no requiere ni una cuenta ni completar un proceso de registro. 

El método GetChart() recibe el gráfico de Google y lo guarda en el 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()");
  }
  

Puede obtener más detalles para trabajar con las herramientas API de Windows y MQL5 del artículo El uso de WinInet.dll para intercambiar datos entre terminales a través de Internet. Por lo tanto, no voy a dedicarle más tiempo. He escrito yo mismo la función importada Convert_PNG() para la conversión de imágenes PNG a BMP. 

Es necesaria, puesto que Google Chart devuelve gráficos en el formato PNG o GIF, y el objeto "graphic label" sólo acepta imágenes BMP. Se puede encontrar el código de las correspondientes funciones de la librería PNG_to_BMP.dll en el archivo DLL_Sources.zip.

Esta función muestra también algunos ejemplos de trabajo con cadenas y archivos mediante la Librería estándar. Los métodos de la clase CString permiten realizar las mismas operaciones que las funciones de cadena (String Functions). La clase CFile es la base para las clases CFileBin y CFileTxt. Con ellas podemos leer y escribir archivos binarios y de texto respectivamente. Los métodos son similares a las funciones para trabajar con archivos.

Por último, describiremos la función CreateGoogleRequest (); crea peticiones desde los datos presentes en el balance:

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

Cabe señalar que las peticiones para el gráfico de balance y el gráfico circular se recogen por separado. El método Append() añade otra cadena después de la última y el método TrimRight() le permite eliminar los caracteres extra, que aparecen al final de cada línea.


6. El ensamblaje final y las pruebas

La clase está lista, vamos a probarla. Vamos a empezar con el 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);
  }

Aquí, creamos de forma dinámica la instancia de clase del Panel, iniciamos el temporizador e inicializamos las variables adicionales. 

A continuación colocamos la función OnDeinit(), en la cual eliminamos el objeto (que llama automáticamente al destructor) y detenemos el temporizador:

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

La función OnCalculate() va a realizar un seguimiento del flujo de las nuevas transacciones, tick por tick, y actualizar la pantalla, si esto sucede: 

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 función OnTimer() hace el seguimiento de los cambios de tamaño de la ventana y, si es necesario, adapta el tamaño del panel, también supervisa las transacciones, igual que OnCalculate(), en el caso de que haya menos de 1 tick 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 y ejecute el indicador:

Figura 3. La vista final del panel

Figura 3. La vista final del panel

Conclusión

Querido lector, espero que haya encontrado algo nuevo en este artículo. He podido probar antes todas las posibilidades de un instrumento tan esplendido como es la Librería estándar, ya que proporciona comodidad, velocidad y un rendimiento de calidad superior. Por supuesto, debe tener algún conocimiento de POO (Programación orientada a objetos).

Buena suerte. 

Para empezar, descomprima el archivo MQL5.rar en la carpeta del terminal y de los permisos para utilizar los DLLs. El archivo DLL_Sources.zip contiene los códigos fuente de las librerías String_Metrics.dll y PNG_to_BMP.dll, las he escrito yo mismo en el entorno Borland C++ Builder con el GDI instalado.