English Русский 中文 Deutsch 日本語 Português
Gráfico del balance de multisímbolos en MetaTrader 5

Gráfico del balance de multisímbolos en MetaTrader 5

MetaTrader 5Ejemplos | 29 marzo 2018, 15:01
3 894 0
Anatoli Kazharski
Anatoli Kazharski

Contenido

Introducción

En uno de los artículos anteriores, hemos considerado la visualización de los gráficos del balance de multisímbolos. Pero desde aquel entonces, han sido desarrolladas muchas bibliotecas MQL que permiten implementar todo eso en el terminal MetaTrader 5 sin usar los programas ajenos.

En este artículo, voy a mostrar el ejemplo de la aplicación con la interfaz gráfica en la que se muestra el gráfico del balance de multisímbolos y la reducción del depósito según los resultados de la última prueba. El historial de las transacciones va a escribirse en el archivo al final de la simulación del Asesor Experto (EA). Luego, se puede leer y visualizar estos datos en los gráficos.

Aparte de eso, en el artículo se presenta la versión del EA en la que el gráfico del balance de multisímbolos se visualiza y se actualiza en la interfaz gráfica directamente durante el trading, así como durante la simulación en modo de visualización.


Desarrollo de la interfaz gráfica

La manera de como conectar y usar la biblioteca EasyAndFast y como crear una interfaz gráfica para su aplicación MQL a través de ella fue demostrada en el artículo Visualizando la optimización de una estrategia comercial en MetaTrader 5 Por eso, pasaremos directamente a la interfaz gráfica según el tema en cuestión. 

Estos son los elementos que van a usarse en la interfaz gráfica.

  • Formulario para los controles.
  • Botón para actualizar los gráficos con los resultados de la última prueba.
  • Gráfico para visualizar el balance de multisímbolos.
  • Gráfico para visualizar las reducciones del depósito (drawdown).
  • Barra de estado para mostrar la información final adicional.

Las declaraciones de los métodos para crear estos elementos se muestran el código de abajo. La implementación de los métodos se encuentra en el archivo de inclusión separado.

//+------------------------------------------------------------------+
//| Clase para crear la aplicación                                   |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Ventana
   CWindow           m_window1;
   //--- Barra de estado
   CStatusBar        m_status_bar;
   //--- Gráficos
   CGraph            m_graph1;
   CGraph            m_graph2;
   //--- Botones
   CButton           m_update_graph;
   //---
public:
   //--- Crea la interfaz gráfica
   bool              CreateGUI(void);
   //---
private:
   //--- Formulario
   bool              CreateWindow(const string text);
   //--- Barra de estado
   bool              CreateStatusBar(const int x_gap,const int y_gap);
   //--- Gráficos
   bool              CreateGraph1(const int x_gap,const int y_gap);
   bool              CreateGraph2(const int x_gap,const int y_gap);
   //--- Botones
   bool              CreateUpdateGraph(const int x_gap,const int y_gap,const string text);
  };
//+------------------------------------------------------------------+
//| Métodos para crear los controles                                 |
//+------------------------------------------------------------------+
#include "CreateGUI.mqh"
//+------------------------------------------------------------------+

El método principal para crear las interfaces gráficas será el siguiente:

//+------------------------------------------------------------------+
//| Crea la interfaz gráfica                                         |
//+------------------------------------------------------------------+
bool CProgram::CreateGUI(void)
  {
//--- Creación del formulario para los controles
   if(!CreateWindow("Expert panel"))
      return(false);
//--- Creación de controles
   if(!CreateStatusBar(1,23))
      return(false);
   if(!CreateGraph1(1,50))
      return(false);
   if(!CreateGraph2(1,159))
      return(false);
   if(!CreateUpdateGraph(7,25,"Update data"))
      return(false);
//--- Terminar la creación de GUI
   CWndEvents::CompletedGUI();
   return(true);
  }

En total, si ahora compilamos el EA y lo cargamos en el gráfico en el terminal, el resultado actual será el siguiente:

 Fig. 1 – Interfaz gráfica del Asesor Experto.

Fig. 1. Interfaz gráfica del Asesor Experto.

A continuación, hablaremos de la escritura de datos en el archivo.


Asesor Experto de multisímbolos para las pruebas

Para las pruebas vamos a usar el EA MACD Sample de la entrega estándar, pero haremos que sea de multisímbolos. El esquema de multisímbolos que se utiliza en esta versión no es exacta. Con los mismos parámetros, el resultado va a ser diferente dependiendo del símbolo en el que va a realizarse la simulación (se elige en los ajustes del Probador de Estrategias). Por eso, este EA está destinado sólo para las pruebas y demostración de los resultados obtenidos en el marco del tema en cuestión.

Próximamente, las nuevas posibilidades para crear los EAs de multisímbolos serán presentadas en las actualizaciones del terminal MetaTrader 5. Entonces, se podrá pensar en la creación la versión final y universal para los EAS de este tipo. Pero si Usted necesita urgentemente un esquema de multisímbolos rápido y preciso, se puede probar la variante que ha sido propuesta en el foro.

Vamos a insertar otro parámetro string en los parámetros externos para especificar los símbolos que serán usados en la prueba:

//--- Parámetros externos
sinput string Symbols           ="EURUSD,USDJPY,GBPUSD,EURCHF"; // Symbols
input  double InpLots           =0.1;                           // Lots
input  int    InpTakeProfit     =167;                           // Take Profit (in pips)
input  int    InpTrailingStop   =97;                            // Trailing Stop Level (in pips)
input  int    InpMACDOpenLevel  =16;                            // MACD open level (in pips)
input  int    InpMACDCloseLevel =19;                            // MACD close level (in pips)
input  int    InpMATrendPeriod  =14;                            // MA trend period

Los símbolos se especifican separados con coma. Los métodos para la lectura de este parámetro están implementados en la clase del programa (CProgram), así como para el chequeo de los símbolos y para la colocación de aquéllos que figuran en la lista del servidor en la Observación del Mercado. Como opción, se puede especificar los símbolos para el trading a través de una lista preparada de antemano en el archivo, tal como ha sido demostrado en el artículo Libro de recetas MQL5: Desarrollar un Asesor Experto multidivisa con un número ilimitado de parámetros. Es más, se puede componer varias listas en el archivo a selección del usuario, y este ejemplo se puede ver en el artículo Guía práctica de MQL5: Reducción del efecto del sobreajuste y el manejo de la falta de cotizaciones. Se puede inventar varios maneras para seleccionar los símbolos y sus listas a través de la interfaz gráfica. Voy a demostrar esta opción en uno de los siguientes artículos.

Antes de verificar los símbolos en la lista general, hay que guardarlos en el array. Luego, pasaremos este array (source_array[]) en el método CProgram::CheckTradeSymbols(). Aquí, repasamos los símbolos especificados en el parámetro externo en el primer ciclo, y luego en el segundo ciclo, comprobamos si figura este símbolo en la lista en el servidor del bróker. Si es así, lo insertamos en la ventana «Observación de Mercado» y en el array de los símbolos verificados. 

Al final del método, si los símbolos no han sido encontrados, va a usarse sólo el símbolo actual del EA.

class CProgram : public CWndEvents
  {
private:
   //--- Comprueba los símbolos para el trading en el array pasado y devuelve el array de los disponibles
   void              CheckTradeSymbols(string &source_array[],string &checked_array[]);
  };
//+------------------------------------------------------------------+
//| Comprueba los símbolos para el trading en el array pasado y      |
//|  devuelve el array de los disponibles                            |
//+------------------------------------------------------------------+
void CProgram::CheckTradeSymbols(string &source_array[],string &checked_array[])
  {
   int symbols_total     =::SymbolsTotal(false);
   int size_source_array =::ArraySize(source_array);
//--- Buscamos los símbolos indicados en la lista general
   for(int i=0; i<size_source_array; i++)
     {
      for(int s=0; s<symbols_total; s++)
        {
         //--- Obtenemos el nombre del símbolo actual en la lista general
         string symbol_name=::SymbolName(s,false);
         //--- Si coincide
         if(symbol_name==source_array[i])
           {
            //--- Colocamos el símbolo en Observación de Mercado
            ::SymbolSelect(symbol_name,true);
            //--- Añadimos al array de los símbolos confirmados
            int size_array=::ArraySize(checked_array);
            ::ArrayResize(checked_array,size_array+1);
            checked_array[size_array]=symbol_name;
            break;
           }
        }
     }
//--- Si los símbolos no han sido encontrados, usamos el símbolo actual
   if(::ArraySize(checked_array)<1)
     {
      ::ArrayResize(checked_array,1);
      checked_array[0]=_Symbol;
     }
  }

El método CProgram::CheckSymbols() se usa para leer el parámetro string externo en el que especifican los símbolos. Aquí, la cadena se divide en el array por el separador ','. En las cadenas obtenidas, los espacios se cortan a ambos lados. Después de eso, el array se envía para la verificación al método CProgram::CheckTradeSymbols(), que hemos considerado antes.

class CProgram : public CWndEvents
  {
private:
   //--- Comprueba y selecciona los símbolos para el trading en el array desde la cadena
   int               CheckSymbols(const string symbols_enum);
  };
//+-------------------------------------------------------------------------------------+
//| Comprueba y selecciona los símbolos para el trading en el array desde la cadena     |
//+-------------------------------------------------------------------------------------+
int CProgram::CheckSymbols(const string symbols_enum)
  {
   if(symbols_enum!="")
      ::Print(__FUNCTION__," > input trade symbols: ",symbols_enum);
//--- Obtenemos los símbolos de la cadena
   string symbols[];
   ushort u_sep=::StringGetCharacter(",",0);
   ::StringSplit(symbols_enum,u_sep,symbols);
//--- Cortamos los espacios por ambos lados
   int elements_total=::ArraySize(symbols);
   for(int e=0; e<elements_total; e++)
     {
      ::StringTrimLeft(symbols[e]);
      ::StringTrimRight(symbols[e]);
     }
//--- Comprobamos los símbolos
   ::ArrayFree(m_symbols);
   CheckTradeSymbols(symbols,m_symbols);
//--- Devolvemos el número de los símbolos para el trading
   return(::ArraySize(m_symbols));
  }

El archivo con la clase de la estrategia comercial se incluye en el archivo con la clase de la aplicación y se crea el array dinámico del array tipo CStrategy

#include "Strategy.mqh"
//+------------------------------------------------------------------+
//| Clase para crear la aplicación                                   |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Array de estrategias
   CStrategy         m_strategy[];
  };

Durante la inicialización, precisamente aquí obtenemos el array de símbolos y su cantidad del parámetro externo. Luego, establecemos el tamaño para el array de estrategias según el número de los símbolos, e inicializamos todas las instancias de las estrategias, pasando el nombre del símbolo en cada una de ellas.

class CProgram : public CWndEvents
  {
private:
   //--- Total de símbolos
   int               m_symbols_total;
  };
//+------------------------------------------------------------------+
//| Inicialización                                                   |
//+------------------------------------------------------------------+
bool CProgram::OnInitEvent(void)
  {
//--- Obtenemos símbolos para el trading
   m_symbols_total=CheckSymbols(Symbols);
//--- Tamaño del array de sistemas de trading
   ::ArrayResize(m_strategy,m_symbols_total);
//--- Inicialización
   for(int i=0; i<m_symbols_total; i++)
     {
      if(!m_strategy[i].OnInitEvent(m_symbols[i]))
         return(false);
     }
//--- Inicialización con éxito
   return(true);
  }

A continuación, hablaremos de la escritura de datos de la última prueba en el archivo.


Escritura de datos en el archivo

Los datos de la última prueba van a guardarse en la carpeta compartida de los terminales. De esta manera, el archivo estará disponible desde cualquier terminal MetaTrader 5. Determinaremos inmediatamente el nombre de la carpeta y del archivo en el constructor:

class CProgram : public CWndEvents
  {
private:
   //--- Ruta hacia el archivo con los resultados de la última prueba
   string            m_last_test_report_path;
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void) : m_symbols_total(0)
  {
//--- Ruta hacia el archivo con los resultados de la última prueba
   m_last_test_report_path=::MQLInfoString(MQL_PROGRAM_NAME)+"\\LastTest.csv";
  }

Vamos a analizar el método CProgram::CreateSymbolBalanceReport(), a través del cual va a realizarse la escritura en el archivo. Para trabajar en este método (así como en el otro del que hablaremos más tarde), vamos a necesitar los arrays de los balances de los símbolos.

//--- Arrays para los balances de todos los símbolos
struct CReportBalance { double m_data[]; };
//+------------------------------------------------------------------+
//| Clase para crear la aplicación                                   |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Arrays de los balances de todos los símbolos
   CReportBalance    m_symbol_balance[];
   //---
private:
   //--- Crea el informe de la simulación para las transacciones en el formato CSV
   void              CreateSymbolBalanceReport(void);
  };
//+---------------------------------------------------------------------------------+
//| Crea el informe de la simulación para las transacciones en el formato CSV       |
//+---------------------------------------------------------------------------------+
void CProgram::CreateSymbolBalanceReport(void)
  {
   ...
  }

Al principio del método, abrimos el archivo para trabajar en la carpeta compartida de los terminales (FILE_COMMON):

...
//--- Creamos el archivo para la escritura de datos en la carpeta compartida del terminal
   int file_handle=::FileOpen(m_last_test_report_path,FILE_CSV|FILE_WRITE|FILE_ANSI|FILE_COMMON);
//--- Si el handle es válido (el archivo se ha creado/ha abierto)
   if(file_handle==INVALID_HANDLE)
     {
      ::Print(__FUNCTION__," > Error creating file: ",::GetLastError());
      return;
     }
...

Necesitaremos unas cuantas variables auxiliares para la formación de algunos valores del informe. En el archivo, vamos a escribir el historial completo de transacciones con los datos que se listan a continuación:

  • Hora de la transacción
  • Símbolo
  • Tipo
  • Dirección
  • Volumen
  • Precio
  • Swap
  • Resultado (ganancias/pérdidas)
  • Reducción (Drawdown)
  • Balance. Esta columna va a contener el balance total, y en las siguientes, habrá balances de los símbolos que participaban en la simulación

Aquí mismo, formamos la primera línea con los encabezados de estos datos:

...
   double max_drawdown    =0.0; // Reducción máxima
   double balance         =0.0; // Balance
   string delimeter       =","; // Separador
   string string_to_write ="";  // Para formar la cadena de caracteres para la escritura
//--- Formamos la línea de encabezados
   string headers="TIME,SYMBOL,DEAL TYPE,ENTRY TYPE,VOLUME,PRICE,SWAP($),PROFIT($),DRAWDOWN(%),BALANCE";
...

Si más de un símbolo participa en la simulación, hay que completar la línea de encabezados con sus nombres. Después de eso, se puede escribir los encabezados (la primera línea) en el archivo

...
//--- Si hay más de un símbolo, rellenamos la línea de encabezados
   int symbols_total=::ArraySize(m_symbols);
   if(symbols_total>1)
     {
      for(int s=0; s<symbols_total; s++)
         ::StringAdd(headers,delimeter+m_symbols[s]);
     }
//--- Escribimos los encabezados del archivo
   ::FileWrite(file_handle,headers);
...

Luego, obtenemos el historial completo de transacciones y su cantidad, y después, establecemos los tamaños para los arrays:

...
//--- Obtenemos el historial completo
   ::HistorySelect(0,LONG_MAX);
//--- Averiguamos el número de transacciones
   int deals_total=::HistoryDealsTotal();
//--- Establecemos el tamaño del array de balances según el número de símbolos
   ::ArrayResize(m_symbol_balance,symbols_total);
//--- Establecemos el tamaño de los arrays de transacciones para cada símbolo
   for(int s=0; s<symbols_total; s++)
      ::ArrayResize(m_symbol_balance[s].m_data,deals_total);
...

Repasamos el historial completo en el ciclo principal y formamos las cadenas para la escritura en el archivo. Al calcular el beneficio, sumamos también el swap y la comisión. Si resulta que hay más de un símbolo, los repasamos en el segundo ciclo y formamos el balance para cada símbolo.

Escribimos los datos en el archivo cadena por cadena. Al final del método, el archivo se cierra.
...
//--- Repasamos en el ciclo y escribimos los datos
   for(int i=0; i<deals_total; i++)
     {
       //--- Obtenemos el ticket de la transacción
      if(!m_deal_info.SelectByIndex(i))
         continue;
      //--- Averiguamos el número de dígitos en el precio
      int digits=(int)::SymbolInfoInteger(m_deal_info.Symbol(),SYMBOL_DIGITS);
      //--- Calculamos el balance final
      balance+=m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
      //--- Formamos la cadena para la escritura mediante la concatenación
      ::StringConcatenate(string_to_write,
                          ::TimeToString(m_deal_info.Time(),TIME_DATE|TIME_MINUTES),delimeter,
                          m_deal_info.Symbol(),delimeter,
                          m_deal_info.TypeDescription(),delimeter,
                          m_deal_info.EntryDescription(),delimeter,
                          ::DoubleToString(m_deal_info.Volume(),2),delimeter,
                          ::DoubleToString(m_deal_info.Price(),digits),delimeter,
                          ::DoubleToString(m_deal_info.Swap(),2),delimeter,
                          ::DoubleToString(m_deal_info.Profit(),2),delimeter,
                          MaxDrawdownToString(i,balance,max_drawdown),delimeter,
                          ::DoubleToString(balance,2));
      //--- Si hay más de un símbolo, escribimos los valores del balance
      if(symbols_total>1)
        {
         //--- Repasamos todos los símbolos
         for(int s=0; s<symbols_total; s++)
           {
            //--- Si los símbolos coinciden y el resultado de la transacción no es nulo
            if(m_deal_info.Symbol()==m_symbols[s] && m_deal_info.Profit()!=0)
               //--- Reflejamos la transacción en el balance con este símbolo. Tomamos en cuenta el swap y la comisión
               m_symbol_balance[s].m_data[i]=m_symbol_balance[s].m_data[i-1]+m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
            //--- De los contrario, escribiremos el valor anterior
            else
              {
               //--- Si el tipo de la transacción «Calculo del balance» (la primera transacción), el balance es el mismo para todos los símbolos
               if(m_deal_info.DealType()==DEAL_TYPE_BALANCE)
                  m_symbol_balance[s].m_data[i]=balance;
               //--- De los contrario, escribimos el valor anterior en el índice actual
               else
                  m_symbol_balance[s].m_data[i]=m_symbol_balance[s].m_data[i-1];
              }
            //--- Añadimos el balance del símbolo a la cadena
            ::StringAdd(string_to_write,delimeter+::DoubleToString(m_symbol_balance[s].m_data[i],2));
           }
        }
      //--- Escribimos la cadena formada
      ::FileWrite(file_handle,string_to_write);
      //--- Reseteo obligatorio de la variable para la siguiente cadena
      string_to_write="";
     }
//--- Cerramos el archivo
   ::FileClose(file_handle);
...

En el proceso de la formación de cadenas (ver el código de arriba) para la escritura en el archivo, se utiliza el método метод CProgram::MaxDrawdownToString() para el cálculo de la reducción total del balance. Al llamarlo por primera vez, la reducción es igual a cero. Recordamos el valor actual del balance como el máximo/mínimo local. En las siguientes llamadas al método, cuando el balance es mayor que el de la memoria, calculamos la reducción según los valores anteriores y actualizamos el máximo local. De los contrario, actualizamos el mínimo local y devolvemos el valor nulo (cadena vacía).

class CProgram : public CWndEvents
  {
private:
   //--- Devuelve la reducción máxima del máximo local
   string            MaxDrawdownToString(const int deal_number,const double balance,double &max_drawdown);
  };
//+------------------------------------------------------------------+
//| Devuelve la reducción máxima del máximo local                    |
//+------------------------------------------------------------------+
string CProgram::MaxDrawdownToString(const int deal_number,const double balance,double &max_drawdown)
  {
//--- Cadena para visualizar en el informe
   string str="";
//--- Para el cálculo del máximo local y la reducción
   static double max=0.0;
   static double min=0.0;
//--- Si es la primera transacción
   if(deal_number==0)
     {
      //--- Todavía no hay reducción
      max_drawdown=0.0;
      //--- Establecemos el punto inicial como máximo local
      max=balance;
      min=balance;
     }
   else
     {
      //--- Si el balance actual es mayor que en la memoria
      if(balance>max)
        {
         //--- Calculamos la reducción para los valores anteriores
         max_drawdown=100-((min/max)*100);
         //--- Actualizamos al máximo local
         max=balance;
         min=balance;
        }
      else
        {
         //--- Devolvemos el valor nulo de la reducción y actualizamos el mínimo
         max_drawdown=0.0;
         min=fmin(min,balance);
        }
     }
//--- Determinamos la cadena para el informe
   str=(max_drawdown==0)? "" : ::DoubleToString(max_drawdown,2);
   return(str);
  }

La estructura del archivo permite abrirlo en Excel. Eso se muestra en la captura de pantalla de abajo:

 Fig. 2 Estructura del archivo del informe.

Fig. 2. Estructura del informe en Excel.

Al final, la llamada al método CProgram::CreateSymbolBalanceReport() para la escritura del informe después de la prueba debe realizarse al final de la prueba:

//+------------------------------------------------------------------+
//| Evento de finalización de la prueba                              |
//+------------------------------------------------------------------+
double CProgram::OnTesterEvent(void)
  {
//--- Escribimos el informe sólo después de la prueba
   if(::MQLInfoInteger(MQL_TESTER) && !::MQLInfoInteger(MQL_OPTIMIZATION) && 
      !::MQLInfoInteger(MQL_VISUAL_MODE) && !::MQLInfoInteger(MQL_FRAME_MODE))
     {
      //--- Formación del informe y la escritura en los archivos
      CreateSymbolBalanceReport();
     }
//---
   return(0.0);
  }

A continuación, hablaremos de la lectura de datos del informe.


Extracción de datos del archivo

Después de todo lo que hemos implementado más arriba, ahora cada comprobación del EA en el Probador de Estrategias va a concluirse con la escritura del informe en el archivo. A continuación, examinaremos los métodos que se usan para leer los datos de este informe. En primer lugar, hay que leer el archivo y colocar su contenido en el array para que sea más cómodo trabajar con él. Para eso se utiliza el método CProgram::ReadFileToArray(). Aquí, abrimos el archivo en el que ha sido escrito el historial de transacciones al final de la prueba del EA. Leemos cíclicamente el archivo hasta la última cadena y rellenamos el array con datos iniciales.

class CProgram : public CWndEvents
  {
private:
   //--- Array para los datos desde el archivo
   string            m_source_data[];
   //--- 
private:
   //--- Lectura del archivo al array pasado
   bool              ReadFileToArray(const int file_handle);
  };
//+------------------------------------------------------------------+
//| Lectura del archivo al array pasado                              |
//+------------------------------------------------------------------+
bool CProgram::ReadFileToArray(const int file_handle)
  {
//--- Abrimos el archivo
   int file_handle=::FileOpen(m_last_test_report_path,FILE_READ|FILE_ANSI|FILE_COMMON);
//--- Salir si el archivo no se ha abierto
   if(file_handle==INVALID_HANDLE)
      return(false);
//--- Liberamos el array
   ::ArrayFree(m_source_data);
//--- Leemos el archivo al array
   while(!::FileIsEnding(file_handle))
     {
      int size=::ArraySize(m_source_data);
      ::ArrayResize(m_source_data,size+1,RESERVE);
      m_source_data[size]=::FileReadString(file_handle);
     }
//--- Cerrar el archivo
   ::FileClose(file_handle);
   return(true);
  }

Necesitaremos el método auxiliar CProgram::GetStartIndex() para determinar el índice de la columna con el título BALANCE. Como argumento, hay que pasarle la cadena con los encabezados donde va a realizarse la búsqueda del nombre de la columna y el array dinámico para los elementos de la cadena dividida por el separador ','.  

class CProgram : public CWndEvents
  {
private:
   //--- Índice inicial de los balances en el informe
   bool              GetBalanceIndex(const string headers);
  };
//+---------------------------------------------------------------------------------+
//| Determinamos el índice a partir del cual hay que empezar a copiar los datos     |
//+---------------------------------------------------------------------------------+
bool CProgram::GetBalanceIndex(const string headers)
  {
//--- Obtenemos los elementos de la cadena por el separador
   string str_elements[];
   ushort u_sep=::StringGetCharacter(",",0);
   ::StringSplit(headers,u_sep,str_elements);
//--- Buscamos la columna 'BALANCE'
   int elements_total=::ArraySize(str_elements);
   for(int e=elements_total-1; e>=0; e--)
     {
      string str=str_elements[e];
      ::StringToUpper(str);
      //--- Si hemos encontrado la columna con el título necesario
      if(str=="BALANCE")
        {
         m_balance_index=e;
         break;
        }
     }
//--- Mostrar el mensaje si la columna 'BALANCE' no ha sido encontrada
   if(m_balance_index==WRONG_VALUE)
     {
      ::Print(__FUNCTION__," > In the report file there is no heading \'BALANCE\' ! ");
      return(false);
     }
//--- Resultado
   return(true);
  }

Los números de las transacciones van a mostrarse en el eje X de ambos gráficos. El rango de datos lo vamos a mostrar como información adicional en el título inferior del gráfico de balances. Para determinar la fecha inicial y final del historial de transacciones, ha sido realizado el método CProgram::GetDateRange(). Se pasan dos variables string por referencia para la fecha inicial y final del historial de transacciones.

class CProgram : public CWndEvents
  {
private:
   //--- Rango de datos
   void              GetDateRange(string &from_date,string &to_date);
  };
//+------------------------------------------------------------------+
//| Obtenemos la fecha inicial y final en el rango de prueba         |
//+------------------------------------------------------------------+
void CProgram::GetDateRange(string &from_date,string &to_date)
  {
//--- Salir si hay menos de 3 cadenas
   int strings_total=::ArraySize(m_source_data);
   if(strings_total<3)
      return;
//--- Obtenemos la fecha inicial y final del informe
   string str_elements[];
   ushort u_sep=::StringGetCharacter(",",0);
//---
   ::StringSplit(m_source_data[1],u_sep,str_elements);
   from_date=str_elements[0];
   ::StringSplit(m_source_data[strings_total-1],u_sep,str_elements);
   to_date=str_elements[0];
  }

Para obtener los datos del balance y reducciones, se usan los métodos CProgram::GetReportDataToArray() y CProgram::AddDrawDown(). El segundo se invoca dentro del primero, y su código es muy corto (ver el código de abajo). Aquí, se traspasa el índice de la transacción y el valor de la reducción que se colocan en los arrays correspondientes, cuyos valores serán visualizados posteriormente en el gráfico. Guardamos el valor de la reducción en el array m_dd_y[], y el índice en el que debemos mostrar este valor lo guardamos en el array m_dd_x[]. De esta manera, si los índices no tienen valores, nada se visualizará en los gráficos (valores vacíos).

class CProgram : public CWndEvents
  {
private:
   //--- Reducciones del balance general
   double            m_dd_x[];
   double            m_dd_y[];
   //--- 
private:
   //--- Añade la reducción a los arrays
   void              AddDrawDown(const int index,const double drawdown);
  };
//+------------------------------------------------------------------+
//| Añade la reducción a los arrays                                  |
//+------------------------------------------------------------------+
void CProgram::AddDrawDown(const int index,const double drawdown)
  {
   int size=::ArraySize(m_dd_y);
   ::ArrayResize(m_dd_y,size+1,RESERVE);
   ::ArrayResize(m_dd_x,size+1,RESERVE);
   m_dd_y[size] =drawdown;
   m_dd_x[size] =(double)index;
  }

Primero, en el método CProgram::GetReportDataToArray(), se determinan los tamaños de los arrays y el número de las series para el gráfico de balances. Luego, inicializamos el array de los encabezados. Luego, se extraen cíclicamente los elementos de cada cadena por el separador, y los datos se colocan en los arrays de reducciones y balances.  

class CProgram : public CWndEvents
  {
private:
   //--- Obtiene los datos de los símbolos del informe
   int               GetReportDataToArray(string &headers[]);
  };
//+------------------------------------------------------------------+
//| Obtiene los datos de los símbolos del informe                    |
//+------------------------------------------------------------------+
int CProgram::GetReportDataToArray(string &headers[])
  {
//--- Obtenemos los elementos de la cadena de encabezados
   string str_elements[];
   ushort u_sep=::StringGetCharacter(",",0);
   ::StringSplit(m_source_data[0],u_sep,str_elements);
//--- Tamaños de arrays
   int strings_total  =::ArraySize(m_source_data);
   int elements_total =::ArraySize(str_elements);
//--- Liberamos los arrays
   ::ArrayFree(m_dd_y);
   ::ArrayFree(m_dd_x);
//--- Obtenemos el número de las series
   int curves_total=elements_total-m_balance_index;
   curves_total=(curves_total<3)? 1 : curves_total;
//--- Establecer el tamaño para los arrays según el número de las series
   ::ArrayResize(headers,curves_total);
   ::ArrayResize(m_symbol_balance,curves_total);
//--- Establecer el tamaño para las series
   for(int i=0; i<curves_total; i++)
      ::ArrayResize(m_symbol_balance[i].m_data,strings_total,RESERVE);
//--- Si hay varios símbolos (obtenemos los encabezados)
   if(curves_total>2)
     {
      for(int i=0,e=m_balance_index; e<elements_total; e++,i++)
         headers[i]=str_elements[e];
     }
   else
      headers[0]=str_elements[m_balance_index];
//--- Obtenemos los datos
   for(int i=1; i<strings_total; i++)
     {
      ::StringSplit(m_source_data[i],u_sep,str_elements);
      //--- Reunimos los datos en los arrays
      if(str_elements[m_balance_index-1]!="")
         AddDrawDown(i,double(str_elements[m_balance_index-1]));
      //--- Si hay varios símbolos
      if(curves_total>2)
         for(int b=0,e=m_balance_index; e<elements_total; e++,b++)
            m_symbol_balance[b].m_data[i]=double(str_elements[e]);
      else
         m_symbol_balance[0].m_data[i]=double(str_elements[m_balance_index]);
     }
//--- El primer valor de las series
   for(int i=0; i<curves_total; i++)
      m_symbol_balance[i].m_data[0]=(strings_total<2)? 0 : m_symbol_balance[i].m_data[1];
//--- Devolver el número de las series
   return(curves_total);
  }

En el siguiente apartado, veamos cómo se visualizan los datos obtenidos en los gráficos.


Visualización de datos en los gráficos

La llamada a los métodos auxiliares considerados en el apartado anterior va a realizarse al principio del método para la actualización del gráfico de balances CProgram::UpdateBalanceGraph(). Luego, las series actuales se eliminan del gráfico porque el número de los símbolos que participan en la última prueba puede cambiarse. Después, según el número actual de los símbolos determinado en el método CProgram::GetReportDataToArray(), añadimos cíclicamente nuevas series de datos de balances y de paso definimos el valor mínimo y máximo por el eje Y. 

Aquí mismo guardamos el tamaño de las series y el paso de divisiones por el eje X en los campos de la clase. Además, vamos a necesitar estos valores para formatear el gráfico de las reducciones. Para el eje Y, se calculan los márgenes para los extremos del gráfico iguales a 5%. Al final, todos estos valores se aplican al gráfico de balances, y el gráfico se actualiza para la visualización de los últimos cambios. 

class CProgram : public CWndEvents
  {
private:
   //--- Total de datos en la serie
   double            m_data_total;
   //--- Paso de graduación en la escala X
   double            m_default_step;
   //--- 
private:
   //--- Actualiza los datos en el gráfico de balances
   void              UpdateBalanceGraph(void);
  };
//+------------------------------------------------------------------+
//| Actualizar el gráfico de balances                                |
//+------------------------------------------------------------------+
void CProgram::UpdateBalanceGraph(void)
  {
//--- Obtenemos los datos del rango de pruebas
   string from_date=NULL,to_date=NULL;
   GetDateRange(from_date,to_date);
//--- Definimos el índice desde el cual hay que empezar a copiar los datso
   if(!GetBalanceIndex(m_source_data[0]))
      return;
//--- Obtienemos los datos de los símbolos del informe
   string headers[];
   int curves_total=GetReportDataToArray(headers);

//--- Actualizar todas las series del gráfico con nuevos datos
   CColorGenerator m_generator;
   CGraphic *graph=m_graph1.GetGraphicPointer();
//--- Vaciar el gráfico
   int total=graph.CurvesTotal();
   for(int i=total-1; i>=0; i--)
      graph.CurveRemoveByIndex(i);
//--- Máximo y mínimo del gráfico
   double y_max=0.0,y_min=m_symbol_balance[0].m_data[0];
//--- Añadimos los datos
   for(int i=0; i<curves_total; i++)
     {
      //--- Definimos el máximo/mínimo por el eje Y
      y_max=::fmax(y_max,m_symbol_balance[i].m_data[::ArrayMaximum(m_symbol_balance[i].m_data)]);
      y_min=::fmin(y_min,m_symbol_balance[i].m_data[::ArrayMinimum(m_symbol_balance[i].m_data)]);
      //--- Añadir la serie al gráfico
      CCurve *curve=graph.CurveAdd(m_symbol_balance[i].m_data,m_generator.Next(),CURVE_LINES,headers[i]);
     }
//--- Número de valores y paso de la cuadrícula del eje X
   m_data_total   =::ArraySize(m_symbol_balance[0].m_data)-1;
   m_default_step =(m_data_total<10)? 1 : ::MathFloor(m_data_total/5.0);
//--- Rango y márgenes
   double range  =::fabs(y_max-y_min);
   double offset =range*0.05;
//--- Color para la primera serie
   graph.CurveGetByIndex(0).Color(::ColorToARGB(clrCornflowerBlue));
//--- Propiedades del eje horizontal
   CAxis *x_axis=graph.XAxis();
   x_axis.AutoScale(false);
   x_axis.Min(0);
   x_axis.Max(m_data_total);
   x_axis.MaxGrace(0);
   x_axis.MinGrace(0);
   x_axis.DefaultStep(m_default_step);
   x_axis.Name(from_date+" - "+to_date);
//--- Propiedades del eje vertical
   CAxis *y_axis=graph.YAxis();
   y_axis.AutoScale(false);
   y_axis.Min(y_min-offset);
   y_axis.Max(y_max+offset);
   y_axis.MaxGrace(0);
   y_axis.MinGrace(0);
   y_axis.DefaultStep(range/10.0);
//--- Actualizar el gráfico
   graph.CurvePlotAll();
   graph.Update();
  }

Para actualizar el gráfico de reducciones, se usa el método CProgram::UpdateDrawdownGraph(). Puesto que los datos ya han sido calculados en el método CProgram::UpdateBalanceGraph(), aquí sólo hay que aplicarlos al gráfico y actualizarlo.

class CProgram : public CWndEvents
  {
private:
   //--- Actualiza los datos en el gráfico de reducciones
   void              UpdateDrawdownGraph(void);
  };
//+------------------------------------------------------------------+
//| Actualizar el gráfico de reducciones                             |
//+------------------------------------------------------------------+
void CProgram::UpdateDrawdownGraph(void)
  {
//--- Actualizamos el gráfico de reducciones
   CGraphic *graph=m_graph2.GetGraphicPointer();
   CCurve *curve=graph.CurveGetByIndex(0);
   curve.Update(m_dd_x,m_dd_y);
   curve.PointsFill(false);
   curve.PointsSize(6);
   curve.PointsType(POINT_CIRCLE);
//--- Propiedades del eje horizontal
   CAxis *x_axis=graph.XAxis();
   x_axis.AutoScale(false);
   x_axis.Min(0);
   x_axis.Max(m_data_total);
   x_axis.MaxGrace(0);
   x_axis.MinGrace(0);
   x_axis.DefaultStep(m_default_step);
//--- Actualizar el gráfico
   graph.CalculateMaxMinValues();
   graph.CurvePlotAll();
   graph.Update();
  }

La llamada a los métodos CProgram::UpdateBalanceGraph() y CProgram::UpdateDrawdownGraph() se realiza en el método CProgram::UpdateGraphs(). Antes de llamar a estos métodos, primero se invoca el método CProgram::ReadFileToArray() que obtiene los datos desde el archivo con los resultados de la última simulación del EA. 

class CProgram : public CWndEvents
  {
private:
   //--- Actualiza los datos en los gráficos de los resultados de la última prueba
   void              UpdateGraphs(void);
  };
//+------------------------------------------------------------------+
//| Actualizar los gráficos                                          |
//+------------------------------------------------------------------+
void CProgram::UpdateGraphs(void)
  {
//--- Llenamos el array con datos desde el archivo
   if(!ReadFileToArray())
     {
      ::Print(__FUNCTION__," > Could not open the test results file!");
      return;
     }
//--- Actualizar el gráfico de balances y reducciones
   UpdateBalanceGraph();
   UpdateDrawdownGraph();
  }

Demostración del resultado obtenido

Para visualizar los resultados de la última prueba en los gráficos de la interfaz, hay que pulsar sólo un botón. El evento de esta acción se procesa en el método CProgram::OnEvent():

//+------------------------------------------------------------------+
//| Manejador de eventos                                             |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Evento del clic en los botones
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- Clic en el botón 'Update data'
      if(lparam==m_update_graph.Id())
        {
         //--- Actualizar gráficos
         UpdateGraphs();
         return;
        }
      //---
      return;
     }
  }

Si el EA ya ha sido simulado antes de pulsar el botón, vemos aproximadamente lo siguiente:

Fig. 3 – Resultado de la última prueba del EA. 

Fig. 3. Resultado de la última prueba del EA.

De esta manera, si el EA ha sido cargado en el gráfico, Usted tiene la posibilidad de ver inmediatamente los cambios en el gráfico de balances de multisímbolos, repasando los resultados de múltiples pruebas después de la optimización de parámetros.

Gráfico del balance de multisímbolos durante el trading y las pruebas

Ahora, vamos a examinar la segunda versión del EA, cuando el gráfico del balance de multisímbolos se dibuja y se actualiza directamente en el proceso del trading.

La interfaz gráfica se queda prácticamente igual que en la versión arriba descrita. La única diferencia consiste en que en vez del botón de actualización habrá calendario desplegable a través del cual se puede especificar a partir de que fecha hay que mostrar el resultado del trading en los gráficos.

El cambio del historial va a comprobarse en el método OnTrade() cuando surja el evento. Para comprobar que el historial se ha completado con una nueva transacción, se usa el método  CProgram::IsLastDealTicket(). En este método, obtenemos el historial a partir de la hora guardada en la memoria después de la última llamada. Luego, comprobamos los tickets de la última transacción y el ticket guardado en la memoria. Si los tickets se diferencian, entonces actualizamos el ticket y la hora de la última transacción en la memoria para la siguiente comprobación, y devolvemos el indicio (true) de que el historial se ha cambiado.

class CProgram : public CWndEvents
  {
private:
   //--- Hora y ticket de la última transacción comprobada
   datetime          m_last_deal_time;
   ulong             m_last_deal_ticket;
   //--- 
private:
   //--- Comprobación de la nueva transacción
   bool              IsLastDealTicket(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void) : m_last_deal_time(NULL),
                           m_last_deal_ticket(WRONG_VALUE)
  {
  }
//+----------------------------------------------------------------------------+
//| Devuelve el evento de la última transacción en el símbolo especificado     |
//+----------------------------------------------------------------------------+
bool CProgram::IsLastDealTicket(void)
  {
//--- Salir si el historial no ha sido obtenido
   if(!::HistorySelect(m_last_deal_time,LONG_MAX))
      return(false);
//--- Obtenemos la propiedad de las transacciones en la lista obtenida
   int total_deals=::HistoryDealsTotal();
//--- Repasamos todas las transacciones en la lista obtenida de la última transacción a la primera
   for(int i=total_deals-1; i>=0; i--)
     {
       //--- Obtenemos el ticket de la transacción
      ulong deal_ticket=::HistoryDealGetTicket(i);
      //--- Si los tickets son iguales, salimos
      if(deal_ticket==m_last_deal_ticket)
         return(false);
      //--- Si los tickets no son iguales, avisamos de ello
      else
        {
         datetime deal_time=(datetime)::HistoryDealGetInteger(deal_ticket,DEAL_TIME);
         //--- Guardamos la hora y el ticket de la última transacción
         m_last_deal_time   =deal_time;
         m_last_deal_ticket =deal_ticket;
         return(true);
        }
     }
//--- Tickets de otro símbolo
   return(false);
  }

Antes de repasar el historial de las transacciones y rellenar los arrays con datos, es necesario determinar qué símbolos se encuentran en el historial y cuántos son en total. Eso es necesario para el establecimiento de los tamaños de arrays. Para eso se utiliza el método CProgram::GetHistorySymbols(). Antes de su llamada, hay que seleccionar el historial en el intervalo necesario. Luego, añadimos los símbolos que encontramos en el historial a la cadena. Para que los símbolos no se repitan en la cadena, hacemos la comprobación de la presencia de la subcadena especificada. Después de eso, añadimos los símbolos encontrados en el historial al array y devolvemos el número de los símbolos.

class CProgram : public CWndEvents
  {
private:
   //--- Array de los símbolos del historial
   string            m_symbols_name[];
   //--- 
private:
   //--- Obtenemos los símbolos del historial de la cuenta y devolvemos su cantidad
   int               GetHistorySymbols(void);
  };
//+------------------------------------------------------------------------------+
//| Obtenemos los símbolos del historial de la cuenta y devolvemos su cantidad   |
//+------------------------------------------------------------------------------+
int CProgram::GetHistorySymbols(void)
  {
   string check_symbols="";
//--- Recorremos cíclicamente por primera vez y obtenemos los símbolos del trading
   int deals_total=::HistoryDealsTotal();
   for(int i=0; i<deals_total; i++)
     {
       //--- Obtenemos el ticket de la transacción
      if(!m_deal_info.SelectByIndex(i))
         continue;
      //--- Si hay nombre del símbolo
      if(m_deal_info.Symbol()=="")
         continue;
      //--- Si todavía no hay esta cadena, la añadimos
      if(::StringFind(check_symbols,m_deal_info.Symbol(),0)==-1)
         ::StringAdd(check_symbols,(check_symbols=="")? m_deal_info.Symbol() : ","+m_deal_info.Symbol());
     }
//--- Obtenemos los elementos de la cadena por el separador
   ushort u_sep=::StringGetCharacter(",",0);
   int symbols_total=::StringSplit(check_symbols,u_sep,m_symbols_name);
//--- Devolvemos el número de los símbolos
   return(symbols_total);
  }

Para obtener el balance de multisímbolos, hay que llamar al método CProgram::GetHistorySymbolsBalance():

class CProgram : public CWndEvents
  {
private:
   //--- Obtener el balance total y balances para cada símbolo separadamente
   void              GetHistorySymbolsBalance(void);
  };
//+-------------------------------------------------------------------------+
//| Obtener el balance total y balances para cada símbolo separadamente     |
//+-------------------------------------------------------------------------+
void CProgram::GetHistorySymbolsBalance(void)
  {
   ...
  }

Aquí, primero, hay que obtener el balance inicial de la cuenta. Obtenemos el historial a partir de la primera transacción, ella representará este balance inicial. Se supone que hay posibilidad de especificar en el calendario la fecha a partir de la cual es necesario mostrar el resultado del trading. Por eso, seleccionamos el historial otra vez. Luego, usando el método CProgram::GetHistorySymbols(), obtenemos los símbolos del historial seleccionado y su cantidad, y después, establecemos los tamaños para los arrays. Para visualizar el intervalo del resultado del historial, definimos la fecha inicial y final

...
//--- Tamaño inicial del depósito
   ::HistorySelect(0,LONG_MAX);
   double balance=(m_deal_info.SelectByIndex(0))? m_deal_info.Profit() : 0;
//--- Obtenemos el historial a partir de la fecha especificada
   ::HistorySelect(m_from_trade.SelectedDate(),LONG_MAX);
//--- Obtenemos la cantidad de los símbolos
   int symbols_total=GetHistorySymbols();
//--- Liberamos los arrays
   ::ArrayFree(m_dd_x);
   ::ArrayFree(m_dd_y);
//--- Establecemos el tamaño del array de balances según el número de los símbolos + 1 del balance total
   ::ArrayResize(m_symbols_balance,(symbols_total>1)? symbols_total+1 : 1);
//--- Establecemos el tamaño de los arrays de transacciones para cada símbolo
   int deals_total=::HistoryDealsTotal();
   for(int s=0; s<=symbols_total; s++)
     {
      if(symbols_total<2 && s>0)
         break;
      //---
      ::ArrayResize(m_symbols_balance[s].m_data,deals_total);
      ::ArrayInitialize(m_symbols_balance[s].m_data,0);
     }
//--- Número de curvas de balances
   int balances_total=::ArraySize(m_symbols_balance);
//--- Inicio y fin del historial
   m_begin_date =(m_deal_info.SelectByIndex(0))? m_deal_info.Time() : m_from_trade.SelectedDate();
   m_end_date   =(m_deal_info.SelectByIndex(deals_total-1))? m_deal_info.Time() : ::TimeCurrent();
...

Los balances de los símbolos y reducciones se calculan en el siguiente ciclo. Los datos obtenidos se colocan el los arrays. Para calcular la reducción, aquí también se usan los métodos descritos en los apartados anteriores.

...
//--- Reducción máxima
   double max_drawdown=0.0;
//--- Escribimos los arrays de balances en el array pasado
   for(int i=0; i<deals_total; i++)
     {
       //--- Obtenemos el ticket de la transacción
      if(!m_deal_info.SelectByIndex(i))
         continue;
      //--- Inicialización en la primera transacción
      if(i==0 && m_deal_info.DealType()==DEAL_TYPE_BALANCE)
         balance=0;
      //--- A partir de la fecha especificada
      if(m_deal_info.Time()>=m_from_trade.SelectedDate())
        {
         //--- Calculamos el balance total
         balance+=m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
         m_symbols_balance[0].m_data[i]=balance;
         //--- Calculamos la reducción
         if(MaxDrawdownToString(i,balance,max_drawdown)!="")
            AddDrawDown(i,max_drawdown);
        }
      //--- Si hay más de un símbolo, escribimos los valores del balance
      if(symbols_total<2)
         continue;
      //--- Sólo a partir de la fecha especificada
      if(m_deal_info.Time()<m_from_trade.SelectedDate())
         continue;
      //--- Repasamos todos los símbolos
      for(int s=1; s<balances_total; s++)
        {
         int prev_i=i-1;
         //--- Si el tipo de la transacción «Calculo del balance» (la primera transacción) ...
         if(prev_i<0 || m_deal_info.DealType()==DEAL_TYPE_BALANCE)
           {
            //--- ... el balance es el mismo para todos los símbolos
            m_symbols_balance[s].m_data[i]=balance;
            continue;
           }
        //--- Si los símbolos coinciden y el resultado de la transacción no es nulo
         if(m_deal_info.Symbol()==m_symbols_name[s-1] && m_deal_info.Profit()!=0)
           {
            //--- Reflejamos la transacción en el balance con este símbolo. Tomamos en cuenta el swap y la comisión.
            m_symbols_balance[s].m_data[i]=m_symbols_balance[s].m_data[prev_i]+m_deal_info.Profit()+m_deal_info.Swap()+m_deal_info.Commission();
           }
         //--- De los contrario, escribiremos el valor anterior
         else
            m_symbols_balance[s].m_data[i]=m_symbols_balance[s].m_data[prev_i];
        }
     }
...

Los métodos CProgram::UpdateBalanceGraph() y CProgram::UpdateDrawdownGraph() sirven para añadir los datos a los gráficos y actualizarlos. Su código es prácticamente es el mismo que en la primera versión del EA considerado en los apartados anteriores, por eso pasamos directamente a la parte donde se invocan.

En primer lugar, estos métodos se invocan cuando se crea la interfaz gráfica, para que el usuario pueda ver inmediatamente el resultado del trading. Después de eso, los gráficos van a actualizarse en el método OnTrade() según vayan apareciendo los eventos comerciales. 

class CProgram : public CWndEvents
  {
private:
  //--- Inicialización de los gráficos
   void              UpdateBalanceGraph(const bool update=false);
   void              UpdateDrawdownGraph(void);
  };
//+------------------------------------------------------------------+
//| Evento de la operación comercial                                 |
//+------------------------------------------------------------------+
void CProgram::OnTradeEvent(void)
  {
//--- Actualización del gráfico de balances y reducciones
   UpdateBalanceGraph();
   UpdateDrawdownGraph();
  }

Aparte de eso, usando la interfaz gráfica, el usuario puede indicar la fecha a partir de la cual es necesario construir el gráfico del balance. Para actualizar forzosamente el gráfico sin comprobar el último ticket de la transacción, hay que pasar el valor true en el método  CProgram::UpdateBalanceGraph().

El evento del cambio de la fecha en el calendario (ON_CHANGE_DATE) se procesa así:

//+------------------------------------------------------------------+
//| Manejador de eventos                                             |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Evento de la selección de la fecha en el calendario
   if(id==CHARTEVENT_CUSTOM+ON_CHANGE_DATE)
     {
      if(lparam==m_from_trade.Id())
        {
         UpdateBalanceGraph(true);
         UpdateDrawdownGraph();
         m_from_trade.ChangeComboBoxCalendarState();
        }
      //---
      return;
     }
  }

A continuación, se muestra cómo funciona eso en el Probador de Estrategias en el modo de visualización:

Fig. 4 – Demostración del resultado en el Probador de Estrategias en el modo de visualización.

Fig. 4. Demostración del resultado en el Probador de Estrategias en el modo de visualización.

Visualizando los informes del servicio «Señales»

Como otro complemento que puede resultar útil para los usuarios, vamos a crear el EA que permitirá visualizar los resultados del trading desde los informes en el servicio Señales.

Váyase a la página de la señal que le interesa y seleccione la pestaña Historial de transacciones:

Fig. 5. Historial de transacciones de la señal.

El enlace para la descarga del archivo CSV con el historial de transacciones se encuentra abajo de esta lista:

 Fig. 6 – Exportar el historial de transacciones en el archivo CSV.

Fig. 6. Exportar el historial de transacciones en el archivo CSV.

Hay que colocar estos archivos para la implementación presentada en la carpeta del terminal \MQL5\Files. Añadimos un parámetro externo al EA. Va a contener el nombre del archivo-informe cuyos datos hay que visualizar en los gráficos.

//+------------------------------------------------------------------+
//|                                                      Program.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- Parámetros externos
input string PathToFile=""; // Path to file
...

Fig. 7 – Parámetro externo para especificar el archivo-informe.

Fig. 7.  Parámetro externo para especificar el archivo-informe.

La interfaz gráfica de esta versión del EA va a contener sólo dos gráficos. Cuando el EA se carga al gráfico en el terminal, intentará abrir el archivo especificado en los ajustes. Si este archivo no se encuentra, el programa mostrará un mensaje en el Registro. El conjunto de los métodos es aproximadamente el mismo que en las versiones arriba descritos. Hay algunas diferencias, pero el principio más o menos es el mismo. Vamos a analizar sólo los métodos donde el enfoque ha cambiado significativamente.

Pues bien, el archivo ha sido leído y las cadenas han sido traspasados de él al array para los datos iniciales. Ahora, hay que distribuir estos datos en el array bidimensional, tal como eso se hace en la tablas. Eso es necesario para un ordenamiento conveniente según la hora de la apertura de transacciones, desde el reciente hasta ulterior. Para eso vamos a necesitar un array de los arrays separado.

//--- Arrays para los datos desde el archivo
struct CReportTable
  {
   string            m_rows[];
  };
//+------------------------------------------------------------------+
//| Clase para crear la aplicación                                   |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Tabla para el informe
   CReportTable      m_columns[];
   //--- Número de filas y columnas
   uint              m_rows_total;
   uint              m_columns_total;
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void) : m_rows_total(0),
                           m_columns_total(0)
  {
...
  }

Para ordenar el array de los arrays, necesitaremos los siguientes métodos:

class CProgram : public CWndEvents
  {
private:
   //--- Método de ordenamiento rápido
   void              QuickSort(uint beg,uint end,uint column);
   //--- Comprobación de condiciones de ordenamiento
   bool              CheckSortCondition(uint column_index,uint row_index,const string check_value,const bool direction);
   //--- Alternar los valores en las celdas especificadas
   void              Swap(uint r1,uint r2);
  };

Todos estos métodos fueron analizados detalladamente en uno de los artículos anteriores

Todas las operaciones principales se realizan en el método CProgram::GetData(). Vamos a detallarlo. .

class CProgram : public CWndEvents
  {
private:
   //--- Obtenemos los datos en los arrays
   int               GetData(void);
  };
//+---------------------------------------------------------+
//| Obtiene los datos de los símbolos del informe           |
//+---------------------------------------------------------+
int CProgram::GetData(void)
  {
...
  }

Primero, vamos a determinar el número de las cadenas y elementos de la cadena por el separador ';'. Luego, obtenemos los nombres de los símbolos y su cantidad en el array separado que figura en el informe. Después de eso, preparamos los arrays y los llenamos con datos desde el informe.

...
//--- Obtenemos los elementos de la cadena de encabezados
   string str_elements[];
   ushort u_sep=::StringGetCharacter(";",0);
   ::StringSplit(m_source_data[0],u_sep,str_elements);
//--- Número de cadenas y elementos de la cadena
   int strings_total  =::ArraySize(m_source_data);
   int elements_total =::ArraySize(str_elements);
//--- Obtenemos los símbolos
   if((m_symbols_total=GetHistorySymbols())==WRONG_VALUE)
     return;
//--- Liberamos los arrays
   ::ArrayFree(m_dd_y);
   ::ArrayFree(m_dd_x);
//--- Tamaño de las filas de datos
   ::ArrayResize(m_columns,elements_total);
   for(int i=0; i<elements_total; i++)
      ::ArrayResize(m_columns[i].m_rows,strings_total-1);
//--- Llenamos los arrays con datos desde el archivo
   for(int r=0; r<strings_total-1; r++)
     {
      ::StringSplit(m_source_data[r+1],u_sep,str_elements);
      for(int c=0; c<elements_total; c++)
         m_columns[c].m_rows[r]=str_elements[c];
     }
...

Todo esta listo para el ordenamiento de datos. Aquí hay que establecer el tamaño de los arrays de balances de los símbolos antes de llenarlos:

...
//--- Número de filas y columnas
   m_rows_total    =strings_total-1;
   m_columns_total =elements_total;
//--- Ordenamos según la hora en la primera columna
   QuickSort(0,m_rows_total-1,0);
//--- Tamaño de series
   ::ArrayResize(m_symbol_balance,m_symbols_total);
   for(int i=0; i<m_symbols_total; i++)
      ::ArrayResize(m_symbol_balance[i].m_data,m_rows_total);
...

Luego, primero llenamos el array del balance total y reducciones. Vamos a omitir todas las transacciones que pertenecen a la carga del depósito.

...
//--- Balance y reducción máxima
   double balance      =0.0;
   double max_drawdown =0.0;
//--- Obtenemos los datos del balance total
   for(uint i=0; i<m_rows_total; i++)
     {
      //--- Balance inicial
      if(i==0)
        {
         balance+=(double)m_columns[elements_total-1].m_rows[i];
         m_symbol_balance[0].m_data[i]=balance;
        }
      else
        {
         //--- Omitimos la reposición
         if(m_columns[1].m_rows[i]=="Balance")
            m_symbol_balance[0].m_data[i]=m_symbol_balance[0].m_data[i-1];
         else
           {
            balance+=(double)m_columns[elements_total-1].m_rows[i]+(double)m_columns[elements_total-2].m_rows[i]+(double)m_columns[elements_total-3].m_rows[i];
            m_symbol_balance[0].m_data[i]=balance;
           }
        }
      //--- Calculamos la reducción
      if(MaxDrawdownToString(i,balance,max_drawdown)!="")
         AddDrawDown(i,max_drawdown);
     }
...

Luego rellenamos los arrays de balances para cada símbolo separado. 

...
//--- Obtenemos los datos de los balances de los símbolos
   for(int s=1; s<m_symbols_total; s++)
     {
      //--- Balance inicial
      balance=m_symbol_balance[0].m_data[0];
      m_symbol_balance[s].m_data[0]=balance;
      //---
      for(uint r=0; r<m_rows_total; r++)
        {
         //--- Si los símbolos no coinciden, entonces el valor anterior
         if(m_symbols_name[s]!=m_columns[m_symbol_index].m_rows[r])
           {
            if(r>0)
               m_symbol_balance[s].m_data[r]=m_symbol_balance[s].m_data[r-1];
            //---
            continue;
           }
         //--- Si el resultado de la transacción no es nulo
         if((double)m_columns[elements_total-1].m_rows[r]!=0)
           {
            balance+=(double)m_columns[elements_total-1].m_rows[r]+(double)m_columns[elements_total-2].m_rows[r]+(double)m_columns[elements_total-3].m_rows[r];
            m_symbol_balance[s].m_data[r]=balance;
           }
         //--- De los contrario, escribiremos el valor anterior
         else
            m_symbol_balance[s].m_data[r]=m_symbol_balance[s].m_data[r-1];
        }
     }
...

Después de eso, los datos se visualizan en los gráficos de la interfaz gráfica. Abajo, se muestran unos ejemplos de parte de diferentes proveedores de las señales:

 Fig. 8 – Demostración del resultado (ejemplo 1).

Fig. 8.  Demostración del resultado (ejemplo 1).

 Fig. 9 – Demostración del resultado (ejemplo 2).

Fig. 9.  Demostración del resultado (ejemplo 2).

 Fig. 10 – Demostración del resultado (ejemplo 3).

Fig. 10. Demostración del resultado (ejemplo 3).

 Fig. 11 – Demostración del resultado (ejemplo 4).

Fig. 11.  Demostración del resultado (ejemplo 4).

Conclusión

En este artículo, se muestra la versión moderna de la aplicación MQL para ver los gráficos del balance de multisímbolos. Antes, para obtener este resultado, había que usar los programas ajenos. Ahora, se puede hacer eso sólo a través de MQL, sin tener que salir del terminal MetaTrader 5.

Más abajo Usted puede descargar los archivos adjuntos para el testeo y el análisis detallado del código presentado en el artículo. Cada versión del programa tiene la siguiente estructura de archivos: 

Nombre del archivo Comentario
MacdSampleMultiSymbols.mq5 EA modificado desde la entrega estándar MACD Sample
Program.mqh Archivo con la clase del programa
CreateGUI.mqh Archivo con la implementación de los métodos desde la clase del programa en el archivo Program.mqh
Strategy.mqh Archivo con la clase modificada de la estrategia MACD Sample (versión de multisímbolos)
FormatString.mqh Archivo con las funciones auxiliares para el formateo de las cadenas

Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/4430

Archivos adjuntos |
MQL5.zip (42.73 KB)
Desarrollando los Asesores Expertos multimódulo Desarrollando los Asesores Expertos multimódulo
El lenguaje de programación MQL permite implementar el concepto del diseño modular de las estrategias comerciales. En este artículo, se muestra el ejemplo del desarrollo del Asesor Experto multimódulo compuesto de los módulos de archivos compilados separadamente.
Simulación de patrones que surgen al comerciar con cestas de parejas de divisas. Parte III Simulación de patrones que surgen al comerciar con cestas de parejas de divisas. Parte III
Terminamos el tema de la simulación de los patrones que surgen al comerciar con cestas de parejas de divisas. En este artículo, presentamos los resultados de la simulación de los patrones que monitorean el movimiento de las divisas de la pareja en relación una a otra.
Creando un feed de noticias personalizado en MetaTrader 5 Creando un feed de noticias personalizado en MetaTrader 5
En el artículo se analiza la posibilidad de crear un feed de noticias flexible, que ofrecezca multitud de opciones para elegir el tipo de noticias y su fuente. El artículo muestra cómo se pueden integrar web API con el terminal MetaTrader 5.
Optimización controlable: el método del recocido Optimización controlable: el método del recocido
En el simulador de estrategias de la plataforma comercial MetaTrader 5 solo existen dos variantes de optimización: la iteración completa de parámetros y el algoritmo genético. En este artículo se propone una nueva variante de optimización de estrategias comerciales: el método del recocido. Se muestra el algoritmo del método, su implementación y su método de inclusión en cualquier asesor. El algoritmo desarrollado se ha puesto a prueba con el asesor Moving Average.