English Русский 中文 Deutsch 日本語 Português
Trabajando con los resultados de la optimización mediante la interfaz gráfica

Trabajando con los resultados de la optimización mediante la interfaz gráfica

MetaTrader 5Probador | 27 abril 2018, 12:05
2 096 0
Anatoli Kazharski
Anatoli Kazharski

Contenido

Introducción

Continuamos desarrollar el tema del procesamiento y el análisis de los resultados de la optimización. En el artículo anterior hemos demostrado cómo visualizar los resultados de la optimización mediante la interfaz gráfica de la aplicación MQL5. Ahora vamos a complicar nuestra tarea: seleccionamos 100 mejores resultados de la optimización y los mostramos en la tabla de la interfaz gráfica. 

Además de eso, continuamos el desarrollo del tema de los gráficos del balance de multisímbolos, que también ha sido presentado en el artículo correspondiente. Vamos a combinar las ideas de estos dos artículos, y hagamos que el usuario obtenga el gráfico del balance de multisímbolos y de la reducción (drawdown) en gráficos separados seleccionando una fila de la tabla de los resultados de la optimización. Así, después de la optimización de los parámetros del Asesor Experto (EA), el trader podrá analizar y seleccionar los resultados que le interesan para el trabajo más rápidamente.

Desarrollo de la interfaz gráfica

La interfaz gráfica del EA de prueba va a incluir los siguientes elementos:

  • Formulario para los controles
  • Barra de estado para mostrar la información final adicional
  • Pestañas para distribuir los controles por grupos:
    • Frames
      • Campo de introducción para controlar la cantidad de los balances mostrados de los resultados durante el desplazamiento (scroll) repetido de los resultados tras la optimización
      • Retardo en milisegundos durante el scroll de los resultados
      • Botón del inicio del scroll repetido de los resultados
      • Gráfico para visualizar la cantidad especificada de los balances de resultados
      • Gráfico para visualizar todos los resultados
    • Results
      • Tabla de los mejores resultados
    • Balance
      • Gráfico para visualizar el balance de multisímbolos del resultado seleccionado en la tabla
      • Gráfico para visualizar las reducciones del resultado seleccionado en la tabla
  • Indicador de la ejecución del proceso de la repetición de los frames

El código de los métodos para crear los elementos arriba mencionados se ubica en un archivo separado y se incluye en el archivo con la clase del programa MQL:

//+------------------------------------------------------------------+
//| Clase para crear la aplicación                                   |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Ventana
   CWindow           m_window1;
   //--- Barra de estado
   CStatusBar        m_status_bar;
   //--- Pestañas
   CTabs             m_tabs1;
   //--- Campos de edición
   CTextEdit         m_curves_total;
   CTextEdit         m_sleep_ms;
   //--- Botones
   CButton           m_reply_frames;
   //--- Gráficos
   CGraph            m_graph1;
   CGraph            m_graph2;
   CGraph            m_graph3;
   CGraph            m_graph4;
   //--- Tablas
   CTable            m_table_param;
   //--- Barra de progreso
   CProgressBar      m_progress_bar;
   //---
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);
   //--- Pestañas
   bool              CreateTabs1(const int x_gap,const int y_gap);
   //--- Campos de edición
   bool              CreateCurvesTotal(const int x_gap,const int y_gap,const string text);
   bool              CreateSleep(const int x_gap,const int y_gap,const string text);
   //--- Botones
   bool              CreateReplyFrames(const int x_gap,const int y_gap,const string text);
   //--- Gráficos
   bool              CreateGraph1(const int x_gap,const int y_gap);
   bool              CreateGraph2(const int x_gap,const int y_gap);
   bool              CreateGraph3(const int x_gap,const int y_gap);
   bool              CreateGraph4(const int x_gap,const int y_gap);
   //--- Botones
   bool              CreateUpdateGraph(const int x_gap,const int y_gap,const string text);
   //--- Tablas
   bool              CreateMainTable(const int x_gap,const int y_gap);
   //--- Barra de progreso
   bool              CreateProgressBar(const int x_gap,const int y_gap,const string text);
  };
//+------------------------------------------------------------------+
//| Métodos para crear los controles                                 |
//+------------------------------------------------------------------+
#include "CreateGUI.mqh"
//+------------------------------------------------------------------+

Como ya he dicho antes, la tabla contendrá los 100 mejores resultados de la optimización (según el beneficio total más grande). Puesto que la interfaz gráfica se crea antes del inicio de la optimización, al principio la tabla estará vacía. El número de las columnas y el texto para los encabezados va a determinarse en la clase del procesamiento de los frames de la optimización.

Creamos la tabla con el siguiente conjunto de las funciones.

  • Visualización de los encabezados
  • Posibilidad de la ordenación
  • Selección de la fila
  • Fijación de la fila seleccionada (sin poder quitar la selección)
  • Cambio del ancho de la columna manualmente
  • Formato en el estilo «cebra»

El código de la creación de la tabla se muestra a continuación. Para asignar la tabla a la segunda pestaña, hay que pasar el objeto de la tabla en el objeto de las pestañas, indicando el índice de la pestaña. En este caso, la clase principal para la tabla será el control «Pestañas». De esta manera, al cambiar los tamaños del área de las pestañas, el tamaño de la tabla va a cambiarse respecto a su control principal, con la condición de que esoestará establecido en las condiciones del control «Tabla».

//+------------------------------------------------------------------+
//| Crea la tabla principal                                          |
//+------------------------------------------------------------------+
bool CProgram::CreateMainTable(const int x_gap,const int y_gap)
  {
//--- Guardamos el puntero al control principal
   m_table_param.MainPointer(m_tabs1);
//--- Adjuntar a la pestaña
   m_tabs1.AddToElementsArray(1,m_table_param);
//--- Propiedades
   m_table_param.TableSize(1,1);
   m_table_param.ShowHeaders(true);
   m_table_param.IsSortMode(true);
   m_table_param.SelectableRow(true);
   m_table_param.IsWithoutDeselect(true);
   m_table_param.ColumnResizeMode(true);
   m_table_param.IsZebraFormatRows(clrWhiteSmoke);
   m_table_param.AutoXResizeMode(true);
   m_table_param.AutoYResizeMode(true);
   m_table_param.AutoXResizeRightOffset(2);
   m_table_param.AutoYResizeBottomOffset(2);
//--- Creamos el control
   if(!m_table_param.CreateTable(x_gap,y_gap))
      return(false);
//--- Añadimos el objeto al array común de los grupos de objetos
   CWndContainer::AddToElementsArray(0,m_table_param);
   return(true);
  }

Guardando los resultados de optimización

Para trabajar con los resultados de la optimización, ha sido implementada la clase CFrameGenerator. Vamos a usar la versión presentada en el artículo Visualizando la optimización de una estrategia comercial en MetaTrader 5. Vamos a modificarla y añadir los métodos necesarios. Tendremos que guardar no sólo el balance general y la estadística final en los frames, sino también el balance y la reducción del depósito para cada símbolo. Para guardar los balances, vamos a usar una estructura de los arrays separada, CSymbolBalance. Ella se usa para dos cosas. Va a almacenar los datos en sus arrays, que luego se pasarán al frame en el array general. Después de la optimización, los datos van a extraerse desde el array del frame y devolverse a los arrays de esta estructura para ser visualizados en los gráficos del balance de multisímbolo.

//--- Arrays para los balances de todos los símbolos
struct CSymbolBalance
  {
   double            m_data[];
  };
//+------------------------------------------------------------------+
//| Clase para trabajar con los resultados de la optimización        |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   //--- Estructura de los balances
   CSymbolBalance    m_symbols_balance[];
  };

Como parámetro string, el frame va a recibir la enumeración de los símbolos separados con ','. Desde el principio, se suponía guardar los datos en el frame, como un informe completo en el array string. Pero en este momento, no se puede pasar los arrays string en el frame. Al intentar pasar un array tipo string a la función FrameAdd(), durante la compilación obtendremos un mensaje de error: los arrays string y estructuras que contienen los objetos no se admiten.

string arrays and structures containing objects are not allowed

Otra opción es escribir el informe en el archivo, y después pasarlo al frame. Pero esta opción tampoco nos conviene: en este caso, tendríamos que escribir los resultados en el disco duro con demasiada frecuencia.

Por eso, he decidido recopilar todos los datos necesarios en un array, y luego extraerlos usando las claves que se guardan en los parámetros del frame. Al principio de este array, van a encontrarse los índices estadísticos. Luego, siguen los datos del balance general, y después de ellos, el balance para cada símbolo por separado. Al final, van a encontrarse los datos de las reducciones para dos ejes por separado. 

En el esquema de abajo, se muestran las secuencias de los datos en el array. Para abreviar, se muestra la variante de dos símbolos.

 


Fig. 1. Secuencia de la distribución de datos en el array.

Para determinar los índices de cada rango dentro de este array, como ha sido dicho antes, necesitaremos las claves. La cantidad de las indicaciones estadísticas es constante y se determina de antemano. En este caso, en la tabla vamos a visualizar cinco indicaciones y el número del repaso con el fin de garantizar la posibilidad del acceso a los datos de este resultado después de la optimización:

//--- Número de indicaciones estadísticas
#define STAT_TOTAL 6

La cantidad de los datos del balance general y para cada símbolo será igual. Vamos a enviar este valor a la función FrameAdd(), como parámetro double. Para determinar qué símbolos han participado en la simulación, vamos a determinarlos en el historial de las transacciones durante cada repaso en la función OnTester(). Esta información será enviada a la función FrameAdd() como parámetro string.

::FrameAdd(m_report_symbols,1,data_count,stat_data);

La secuencia de los símbolos especificados en el parámetro string coincide con la secuencia de los datos en el array. De esta manera, teniendo todos estos parámetros, se puede extraer todos los datos enpaquetados en el array, sin confundir nada. 

En el código de abajo se muestra el método CFrameGenerator::GetHistorySymbols(), que sirve para determinar los símbolos en el historial de las transacciones:

#include <Trade\DealInfo.mqh>
//+------------------------------------------------------------------+
//| Clase para trabajar con los resultados de la optimización        |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   //--- Trabajo con transacciones
   CDealInfo         m_deal_info;
   //--- Símbolos del informe
   string            m_report_symbols;
   //---
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 CFrameGenerator::GetHistorySymbols(void)
  {
//--- 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(m_report_symbols,m_deal_info.Symbol(),0)==-1)
         ::StringAdd(m_report_symbols,(m_report_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(m_report_symbols,u_sep,m_symbols_name);
//--- Devolvemos el número de los símbolos
   return(symbols_total);
  }

Si resulta que en el historial de las transacciones hay más de un símbolo, el tamaño del array se establece a un elemento menos. El primer elemento se reserva para el balance general. 

//--- 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,(m_symbols_total>1)? m_symbols_total+1 : 1);

Después de que los datos desde el historial de las transacciones hayan sido guardados en los arrays separados, hay que colocarlos en un array general. Para eso, se utiliza el método CFrameGenerator::CopyDataToMainArray(). Aquí, aumentamos cíclicamente el array general por la cantidad de datos agregados, y copiamos los datos de las reducciones en la última iteración.

class CFrameGenerator
  {
private:
   //--- Balance del resultado
   double            m_balances[];
   //---
private:
   //--- Copia los datos del balance al array general
   void              CopyDataToMainArray(void);
  };
//+------------------------------------------------------------------+
//| Copia los datos del balance al array general                     |
//+------------------------------------------------------------------+
void CFrameGenerator::CopyDataToMainArray(void)
  {
//--- Número de curvas de balances
   int balances_total=::ArraySize(m_symbols_balance);
//--- Tamaño del array del balance
   int data_total=::ArraySize(m_symbols_balance[0].m_data);
//--- Llenamos el array general con datos
   for(int i=0; i<=balances_total; i++)
     {
      //--- Tamaño actual del balance
      int array_size=::ArraySize(m_balances);
      //--- Copiamos los balances al array
      if(i<balances_total)
        {
         //--- Copiamos el balance al array
         ::ArrayResize(m_balances,array_size+data_total);
         ::ArrayCopy(m_balances,m_symbols_balance[i].m_data,array_size);
        }
      //--- Copiamos las reducciones al array
      else
        {
         data_total=::ArraySize(m_dd_x);
         ::ArrayResize(m_balances,array_size+(data_total*2));
         ::ArrayCopy(m_balances,m_dd_x,array_size);
         ::ArrayCopy(m_balances,m_dd_y,array_size+data_total);
        }
     }
  }

Las indicaciones estadísticas se añaden al principio del array general a través del método CFrameGenerator::GetStatData(). Este método recibe por referencia el array que al final será guardado en el frame. Se le establece el tamaño del array de datos de los balances más el número de indicaciones estadísticas. Los datos de los balances se colocan desde el último índice en el rango de las indicaciones estadísticas. 

class CFrameGenerator
  {
private:
   //--- Recibe los datos estadísticos
   void              GetStatData(double &dst_array[],double on_tester_value);
  };
//+------------------------------------------------------------------+
//| Recibe los datos estadísticos                                    |
//+------------------------------------------------------------------+
void CFrameGenerator::GetStatData(double &dst_array[],double on_tester_value)
  {
//--- Copiar el array
   ::ArrayResize(dst_array,::ArraySize(m_balances)+STAT_TOTAL);
   ::ArrayCopy(dst_array,m_balances,STAT_TOTAL,0);
//--- Llenamos los primeros valores del array (STAT_TOTAL) con los resultados de la simulación
   dst_array[0] =0;                                             // número del repaso
   dst_array[1] =on_tester_value;                               // valor del criterio personalizado de la optimización 
   dst_array[2] =::TesterStatistics(STAT_PROFIT);               // beneficio neto
   dst_array[3] =::TesterStatistics(STAT_TRADES);               // número de transacciones
   dst_array[4] =::TesterStatistics(STAT_EQUITY_DDREL_PERCENT); // reducción máxima de equidad en por cientos
   dst_array[5] =::TesterStatistics(STAT_RECOVERY_FACTOR);      // factor de recuperación
  }

Al final, las acciones arriba descritas se realizan en el método CFrameGenerator::OnTesterEvent(), que se invoca en el archivo principal del programa en la función OnTester()

//+---------------------------------------------------------------------------+
//| Prepara el array de valores del balance y lo envía dentro del frame       |
//| La función debe invocarse en el EA en el manejador OnTester()             |
//+---------------------------------------------------------------------------+
void CFrameGenerator::OnTesterEvent(const double on_tester_value)
  {
//--- Obtenemos los datos del balance
   int data_count=GetBalanceData();
//--- Array para enviar los datos en el frame
   double stat_data[];
   GetStatData(stat_data,on_tester_value);
//--- Creamos el frame con los datos y lo enviamos al terminal
   if(!::FrameAdd(m_report_symbols,1,data_count,stat_data))
      ::Print(__FUNCTION__," > Frame add error: ",::GetLastError());
   else
      ::Print(__FUNCTION__," > Frame added, OK");
  }

Los arrays de la tabla van a rellenarse el final de la optimización en el método FinalRecalculateFrames(), que se invoca en el método CFrameGenerator::OnTesterDeinitEvent(). Aquí, se realiza el recálculo final de los resultados de la optimización, se determina la cantidad de los parámetros optimizados, se rellena el array de los encabezados de la tabla, se recopilan los datos en el array de la tabla. Después de eso, los datos se ordenan según el criterio especificado. 

Vamos a considerar unos métodos auxiliares, los cuales van a invocarse en el ciclo final del procesamiento de los frames. Empezamos con el método CFrameGenerator::GetParametersTotal(), en el que se determina el número de parámetros del EA que participan en la optimización.

Para obtener los parámetros del EA desde el frame, llamamos a la función FrameInputs(). Cuando pasamos el número del repaso a esta función, obtenemos el array de los parámetros y su cantidad. Al principio de su lista figuran los que han participado en la optimización, y luego van los demás. Puesto que en la tabla se muestran sólo los parámetros optimizados, hay que determinar el índice del primer parámetro no optimizado, con el fin de descartar el grupo que no tiene que entrar en la tabla. En nuestro caso, se puede indicar de antemano el primer parámetro externo no optimizado del EA al que va a orientarse el programa. En este caso, es Symbols. Una vez determinado el índice, se puede calcular la cantidad de los parámetros optimizados del EA.

class CFrameGenerator
  {
private:
   //--- Primer parámetro no optimizado
   string            m_first_not_opt_param;
   //---
private:
   //--- Obtiene la cantidad de parámetros optimizados
   void              GetParametersTotal(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CFrameGenerator::CFrameGenerator(void) : m_first_not_opt_param("Symbols")
  {
  }
//+------------------------------------------------------------------+
//| Obtiene la cantidad de parámetros optimizados                    |
//+------------------------------------------------------------------+
void CFrameGenerator::GetParametersTotal(void)
  {
//--- En el primer frame, determinamos la cantidad de parámetros optimizados
   if(m_frames_counter<1)
     {
      //--- Obtenemos los parámetros de entrada del EA para los que ha sido formado el frame
      ::FrameInputs(m_pass,m_param_data,m_par_count);
      //--- Encontramos el índice del primer parámetro no optimizado
      int limit_index=0;
      int params_total=::ArraySize(m_param_data);
      for(int i=0; i<params_total; i++)
        {
         if(::StringFind(m_param_data[i],m_first_not_opt_param)>-1)
           {
            limit_index=i;
            break;
           }
        }
      //--- Cantidad de parámetros optimizados
      m_param_total=(m_par_count-(m_par_count-limit_index));
     }
  }

Los datos de la table van a almacenarse en la estructura de los arrays CReportTable. Después de averiguar el número de los parámetros optimizados del EA, aparece la posibilidad de determinar y establecer el número de las columnas de la tabla. Eso se hace en el método CFrameGenerator::SetColumnsTotal(). Originalmente, el número de las filas es igual a cero

//--- Arrays de la tabla
struct CReportTable
  {
   string            m_rows[];
  };
//+------------------------------------------------------------------+
//| Clase para el trabajo con los resultados de la optimización      | 
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   //--- Tabla para el informe
   CReportTable      m_columns[];
   //---
private:
   //--- Establecer el número de las columnas de la tabla
   void              SetColumnsTotal(void);
  };
//+------------------------------------------------------------------+
//| Establecer el número de las columnas de la tabla                 |
//+------------------------------------------------------------------+
void CFrameGenerator::SetColumnsTotal(void)
  {
//--- Determinamos el número de las columnas para la tabla de resultados
   if(m_frames_counter<1)
     {
      int columns_total=int(STAT_TOTAL+m_param_total);
      ::ArrayResize(m_columns,columns_total);
      for(int i=0; i<columns_total; i++)
         ::ArrayFree(m_columns[i].m_rows);
     }
  }

Las filas se añaden en el método CFrameGenerator::AddRow(). Durante el proceso del repaso de todos los frames, en la tabla entrarán sólo los resultados que contienen las transacciones. En las primeras columnas de la tabla, empezando desde el número del repaso, van a ubicarse las indicaciones estadísticas, y luego, los parámetros optimizados del EA. Cuando se obtienen los parámetros desde el frame, se muestran en el siguiente formato "parameterN=valueN" [nombre del parámetro][separador][valor del parámetro]. Necesitamos sólo los valores de los parámetros que tienen que entrar en la tabla. Por eso, dividimos la cadena por el separador ‘=’ y guardamos el valor desde el segundo elemento del array.

class CFrameGenerator
  {
private:
   //--- Añade la fila de los datos
   void              AddRow(void);
  };
//+------------------------------------------------------------------+
//| Añade la fila de los datos                                       |
//+------------------------------------------------------------------+
void CFrameGenerator::AddRow(void)
  {
//--- Establecemos el número de las columnas en la tabla
   SetColumnsTotal();
//--- Salir si no hay transacciones
   if(m_data[3]<1)
      return;
//--- Llenamos la tabla
   int columns_total=::ArraySize(m_columns);
   for(int i=0; i<columns_total; i++)
     {
      //--- Añadimos la fila
      int prev_rows_total=::ArraySize(m_columns[i].m_rows);
      ::ArrayResize(m_columns[i].m_rows,prev_rows_total+1,RESERVE);
      //--- Número del repaso
      if(i==0)
        {
         m_columns[i].m_rows[prev_rows_total]=string(m_pass);
         continue;
        }
      //--- Indicaciones estadísticas
      if(i<STAT_TOTAL)
         m_columns[i].m_rows[prev_rows_total]=string(m_data[i]);
      //--- Parámetros optimizados del EA
      else
        {
         string array[];
         if(::StringSplit(m_param_data[i-STAT_TOTAL],'=',array)==2)
            m_columns[i].m_rows[prev_rows_total]=array[1];
        }
     }
  }

Recogemos los encabezados para la tabla en el método CFrameGenerator::GetHeaders(), pero sólo el primer elemento desde el array de los elementos de la cadena dividida:

class CFrameGenerator
  {
private:
   //--- Obtiene los encabezados para la tabla
   void              GetHeaders(void);
  };
//+------------------------------------------------------------------+
//| Obtiene los encabezados para la tabla                            |
//+------------------------------------------------------------------+
void CFrameGenerator::GetHeaders(void)
  {
   int columns_total =::ArraySize(m_columns);
//--- Encabezados
   ::ArrayResize(m_headers,STAT_TOTAL+m_param_total);
   for(int c=STAT_TOTAL; c<columns_total; c++)
     {
      string array[];
      if(::StringSplit(m_param_data[c-STAT_TOTAL],'=',array)==2)
         m_headers[c]=array[0];
     }
  }

Para indicar al programa según qué criterio hay que elegir 100 resultados para la tabla, usamos un método simple CFrameGenerator::ColumnSortIndex(). Le pasamos el índice de la columna. Tras la finalización de la optimización, la tabla de los resultados será ordenada en orden descendiente precisamente por este índice, mientras que los 100 resultados superiores entrarán en la tabla para visualizarse en la interfaz gráfica. Por defecto, se establece la tercera columna (índice 2), es decir, la ordenación será por el beneficio máximo.

class CFrameGenerator
  {
private:
   //--- Indice de la columna ordenada
   uint              m_column_sort_index;
   //---
public:
   //--- Establecer el índice de la columna por la que se realiza la ordenación de la tabla
   void              ColumnSortIndex(const uint index) { m_column_sort_index=index; }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CFrameGenerator::CFrameGenerator(void) : m_column_sort_index(2)
  {
  }

Si queremos seleccionar los resultados según otro criterio, el método CFrameGenerator::ColumnSortIndex() se invoca en el método CProgram::OnTesterInitEvent() al principio de la optimización:

//+------------------------------------------------------------------+
//| Evento del inicio del proceso de la optimización                 |
//+------------------------------------------------------------------+
void CProgram::OnTesterInitEvent(void)
  {
...
   m_frame_gen.ColumnSortIndex(3);
...
  }

Como resultado, el método CFrameGenerator::FinalRecalculateFrames() para el recálculo final ahora trabaja según el siguiente algoritmo.

  • Pasamos el puntero de los frames al principio de la lista. El reseteo del contador del frame y anulación de los arrays. 
  • Luego, recorremos cíclicamente todos los frames y:
    • obtenemos la cantidad de parámetros optimizados, 
    • distribuimos los resultados negativos y positivos por los arrays, 
    • añadimos la fila de datos a la tabla.
  • Después del ciclo del recorrido por los frames, obtenemos los encabezados de la tabla.
  • Luego, ordenamos la tabla por la columna especificada en los ajustes.
  • Terminamos el método de la actualización del gráfico con los resultados de la optimización.

Código del método CFrameGenerator::FinalRecalculateFrames():

class CFrameGenerator
  {
private:
   //--- Recálculo final de datos de todos los frames tras la optimización
   void              FinalRecalculateFrames(void);
  };
//+------------------------------------------------------------------------+
//| Recálculo final de datos de todos los frames tras la optimización      |
//+------------------------------------------------------------------------+
void CFrameGenerator::FinalRecalculateFrames(void)
  {
//--- Pasamos el puntero de los frames al principio
   ::FrameFirst();
//--- Reseteo del contador y los arrays
   ArraysFree();
   m_frames_counter=0;
//--- Iniciamos el recorrido de los frames
   while(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
      //--- Obtiene la cantidad de parámetros optimizados
      GetParametersTotal();
      //--- Resultado negativo
      if(m_data[m_profit_index]<0)
         AddLoss(m_data[m_profit_index]);
      //--- Resultado positivo
      else
         AddProfit(m_data[m_profit_index]);
      //--- Añade la fila de los datos
      AddRow();
      //--- Aumentamos el contador de frames procesados
      m_frames_counter++;
     }
//--- Obtenemos los encabezados para la tabla
   GetHeaders();
//--- Número de columnas y filas
   int rows_total =::ArraySize(m_columns[0].m_rows);
//--- Ordenamos la tabla por la columna especificada
   QuickSort(0,rows_total-1,m_column_sort_index);
//--- Actualizamos las series en el gráfico
   CCurve *curve=m_graph_results.CurveGetByIndex(0);
   curve.Name("P: "+(string)ProfitsTotal());
   curve.Update(m_profit_x,m_profit_y);
//---
   curve=m_graph_results.CurveGetByIndex(1);
   curve.Name("L: "+(string)LossesTotal());
   curve.Update(m_loss_x,m_loss_y);
//--- Propiedades del eje horizontal
   CAxis *x_axis=m_graph_results.XAxis();
   x_axis.Min(0);
   x_axis.Max(m_frames_counter);
   x_axis.DefaultStep((int)(m_frames_counter/8.0));
//--- Actualizar el gráfico
   m_graph_results.CalculateMaxMinValues();
   m_graph_results.CurvePlotAll();
   m_graph_results.Update();
  }

A continuación, examinaremos los métodos que permiten obtener los datos desde los frames por la solicitud del usuario.

Extracción de datos del frame

Antes, hemos analizado la estructura del array general con la secuencia de datos de categorías diferentes. Ahora, hay que comprender de qué manera van a extraerse los datos desde este array. Más arriba hemos dicho que el frame contiene la secuencia de símbolos y el tamaño de los arrays del balance en forma de las claves. Si el tamaño de los arrays de balances fuera igual al tamaño de los arrays de reducciones, la determinación de los índices de todos los rangos de datos enpaquetados se realizaría según la misma fórmula en el ciclo (tal como se muestra en la esquema de abajo). Pero los tamaños de arrays son diferentes. Por eso, en la última iteración en el ciclo, hay que determinar cuántos elementos quedan en el rango de datos que corresponde a las reducciones del depósito, y dividirlo en dos, por que los tamaños de los arrays de reducciones son iguales. 

 


Fig. 2. Esquema de parámetros para el cálculo del índice del array de la siguiente categoría.

Para obtener los datos desde el frame, ha sido implementado el método CFrameGenerator::GetFrameData(). Vamos a analizarlo más detalladamente.

Al principio del método, hay que pasar el puntero de los frames al principio de la lista. Luego, iniciamos el proceso del recorrido de todos los frames con los resultados de la optimización. Hay que encontrar el frame cuyo número del recorrido ha sido pasado al método como argumento. Si ha sido encontrado, el programa sigue este algoritmo.

  • Obtenemos el tamaño del array general con datos del frame. 
  • Obtenemos los elementos de la cadena del parámetro string y su cantidad. Si resulta que hay más de un símbolo, el número de los balances en el array será más a uno. Es decir, el primer rango es el balance general, los demás corresponden a los balances de los símbolos.
  • Luego, hay que pasar los datos en los arrays de los balances. Iniciamos el ciclo para extraer los datos desde el array general (el número de iteraciones es igual al número de balances). Para determinar el índice a partir del cual es necesario copiar los datos, será suficiente hacer el desplazamiento según el número de las indicaciones estadísticas (STAT_TOTAL) y multiplicar el índice de la iteración ( ) por el tamaño del array del balance (m_value). Así, en cada iteración, obtenemos los datos de todos los balances en los arrays separados.
  • En la última iteración, obtenemos los datos de las reducciones en los arrays separados. Son los últimos datos en el array, por eso simplemente hay que averiguar la cantidad restante de los elementos y dividirla por 2. Luego, obtenemos consecutivamente los datos de las reducciones en dos pasos
  • En el último paso, actualizamos los gráficos con datos nuevos, y detenemos el ciclo del recorrido de frames.
class CFrameGenerator
  {
public:
   //--- Obtiene los datos según el número del frame especificado
   void              GetFrameData(const ulong pass_number);
  };
//+------------------------------------------------------------------+
//| Obtiene los datos según el número del frame especificado         |
//+------------------------------------------------------------------+
void CFrameGenerator::GetFrameData(const ulong pass_number)
  {
//--- Pasamos el puntero de los frames al principio
   ::FrameFirst();
//--- Extracción de datos
   while(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
      //--- Los números de recorridos no coinciden, pasamos al siguiente
      if(m_pass!=pass_number)
         continue;
      //--- Tamaño del array con datos
      int data_total=::ArraySize(m_data);
      //--- Obtenemos los elementos de la cadena por el separador
      ushort u_sep          =::StringGetCharacter(",",0);
      int    symbols_total  =::StringSplit(m_name,u_sep,m_symbols_name);
      int    balances_total =(symbols_total>1)? symbols_total+1 : symbols_total;
      //--- Establecemos el tamaño para el array del número de balances
      ::ArrayResize(m_symbols_balance,balances_total);
      //--- Distribuimos los datos en los arrays
      for(int i=0; i<balances_total; i++)
        {
        //--- Liberar el array de datos
         ::ArrayFree(m_symbols_balance[i].m_data);
         //--- Determinamos el índice a partir del cual hay que copiar los datos
         int src_index=STAT_TOTAL+int(i*m_value);
         //--- Copiamos los datos al array de la estructura de balances
         ::ArrayCopy(m_symbols_balance[i].m_data,m_data,0,src_index,(int)m_value);
         //--- Si se trata de la última iteración, obtenemos los datos de las reducciones
         if(i+1==balances_total)
           {
            //--- Obtenemos la cantidad de datos restantes y el tamaño para los arrays según dos ejes
            double dd_total   =data_total-(src_index+(int)m_value);
            double array_size =dd_total/2.0;
            //--- Indice desde el cual empezamos a copiar
            src_index=int(data_total-dd_total);
            //--- Establecemos el tamaño para los arrays de reducciones
            ::ArrayResize(m_dd_x,(int)array_size);
            ::ArrayResize(m_dd_y,(int)array_size);
            //--- Copiamos consecutivamente los datos
            ::ArrayCopy(m_dd_x,m_data,0,src_index,(int)array_size);
            ::ArrayCopy(m_dd_y,m_data,0,src_index+(int)array_size,(int)array_size);
           }
        }
      //--- Actualizar los gráficos y detener el ciclo
      UpdateMSBalanceGraph();
      UpdateDrawdownGraph();
      break;
     }
  }

Para obtener los datos desde las celdas del array de la tabla, llamamos al método público CFrameGenerator::GetValue(), indicando el índice de la columna y de la fila de la tabla en los argumentos. 

class CFrameGenerator
  {
public:
   //--- Devuelve los valores de la celda especificada
   string            GetValue(const uint column_index,const uint row_index);
  };
//+------------------------------------------------------------------+
//| Devuelve los valores de la celda especificada                    |
//+------------------------------------------------------------------+
string CFrameGenerator::GetValue(const uint column_index,const uint row_index)
  {
//--- Comprobar la superación del rango de las columnas
   uint csize=::ArraySize(m_columns);
   if(csize<1 || column_index>=csize)
      return("");
//--- Comprobar la superación del rango de las filas
   uint rsize=::ArraySize(m_columns[column_index].m_rows);
   if(rsize<1 || row_index>=rsize)
      return("");
//---
   return(m_columns[column_index].m_rows[row_index]);
  }

Visualización de datos e interacción con la interfaz gráfica

Para actualizar los gráficos con los datos de los balances y reducciones, en la clase CFrameGenerator han sido declarados dos objetos más del tipo CGraphic. Igual como en el caso de otros objetos de este tipo, en la clase CFrameGenerator, es necesario completarlos con los punteros a los elementos de la interfaz gráfica al principio de la optimización en el método CFrameGenerator::OnTesterInitEvent(). 

#include <Graphics\Graphic.mqh>
//+------------------------------------------------------------------+
//| Clase para trabajar con los resultados de la optimización        |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   //--- Punteros a los gráficos para visualizar los datos
   CGraphic         *m_graph_ms_balance;
   CGraphic         *m_graph_drawdown;
   //---
public:
   //--- Manejadores de los eventos del Probador de Estrategias
   void              OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_results,CGraphic *graph_ms_balance,CGraphic *graph_drawdown);
  };
//+------------------------------------------------------------------+
//| Debe invocarse en el manejador OnTesterInit()                    |
//+------------------------------------------------------------------+
void CFrameGenerator::OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_results,
                                        CGraphic *graph_ms_balance,CGraphic *graph_drawdown)
  {
   m_graph_balance    =graph_balance;
   m_graph_results    =graph_results;
   m_graph_ms_balance =graph_ms_balance;
   m_graph_drawdown   =graph_drawdown;
  }

Los datos de la tabla de la interfaz gráfica se visualizan a través del método CProgram::GetFrameDataToTable(). Definimos la cantidad de columnas obteniendo los encabezados de la tabla desde el objeto CFrameGenerator en el array. Después de eso, establecemos el tamaño de la tabla (100 filas) en la interfaz gráfica. Luego, establecemos los encabezados y el tipo de datos.

Ahora, es necesario inicializar la tabla con los resultados de la optimización. Establecemos los valores usando el método CTable::SetValue(). Para obtener los valores desde las celdas de la tabla de datos, se usa el método CFrameGenerator::GetValue(). Para que los cambios realizados se muestren, es necesario actualizar la tabla.

class CProgram
  {
private:
   //--- Obtiene los datos de los frames en la tabla de los resultados de la optimización
   void              GetFrameDataToTable(void);
  };
//+------------------------------------------------------------------------+
//| Obtenemos los datos en la tabla de los resultados de la optimización   |
//+------------------------------------------------------------------------+
void CProgram::GetFrameDataToTable(void)
  {
//--- Obtenemos los encabezados
   string headers[];
   m_frame_gen.CopyHeaders(headers);
//--- Establecemos el tamaño de la tabla
   uint columns_total=::ArraySize(headers);
   m_table_param.Rebuilding(columns_total,100,true);
//--- Establecemos los encabezados y el tipo de datos
   for(uint c=0; c<columns_total; c++)
     {
      m_table_param.DataType(c,TYPE_DOUBLE);
      m_table_param.SetHeaderText(c,headers[c]);
     }
//--- Llenamos la tabla con datos desde los frames
   for(uint c=0; c<columns_total; c++)
     {
      for(uint r=0; r<m_table_param.RowsTotal(); r++)
        {
         if(c==1 || c==2 || c==4 || c==5)
            m_table_param.SetValue(c,r,m_frame_gen.GetValue(c,r),2);
         else
            m_table_param.SetValue(c,r,m_frame_gen.GetValue(c,r),0);
        }
     }
//--- Actualizar la tabla
   m_table_param.Update(true);
   m_table_param.GetScrollHPointer().Update(true);
   m_table_param.GetScrollVPointer().Update(true);
  }

El método CProgram::GetFrameDataToTable() se invoca cuando se finaliza el proceso de la optimización de los parámetros del Asesor Experto en el método OnTesterDeinit(). Después de eso, la interfaz gráfica se hace disponible para el usuario. Si va a la pestaña Results, puede observar los resultados de la optimización seleccionados según el criterio especificado. En nuestro ejemplo, la selección se realiza según la indicación en la segunda columna (Profit).

 Fig. 3. Tabla de resultados de la optimización en la interfaz gráfica.

Fig. 3. Tabla de resultados de la optimización en la interfaz gráfica.

Ahora, veremos de qué manera el usuario puede ver los balances de multisímbolos de los resultados en esta tabla. Si seleccionamos alguna fila de la tabla, se genera el evento de usuario ON_CLICK_LIST_ITEM con el identificador de la tabla. Según él, podemos determinar de qué tabla ha sido recibido este mensaje (si hay más de una). Puesto que en la primera columna de la tabla se guarda el número del repaso, se puede obtener los datos de este resultado, pasando este número al método CFrameGenerator::GetFrameData().

//+------------------------------------------------------------------+
//| Manejador de eventos                                             |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Evento del clic en las filas de la tabla
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM)
     {
      if(lparam==m_table_param.Id())
        {
         //--- Obtenemos el número del repaso desde la tabla 
         ulong pass=(ulong)m_table_param.GetValue(0,m_table_param.SelectedItem());
         //--- Obtenemos los datos según el número del repaso
         m_frame_gen.GetFrameData(pass);
        }
      //---
      return;
     }
...
  }

Cada vez cuando el usuario seleccione una fila en la tabla, el gráfico de los balances de multisímbolos se actualiza en la pestaña Balance:

 Fig. 4. Demostración del resultado obtenido.

Fig. 4. Demostración del resultado obtenido.

Hemos obtenido una herramienta bastante cómoda para revisar rápidamente los resultados de multisímbolos de las pruebas. 

Conclusión

He mostrado una opción más de las posibles de qué manera se puede trabajar con los resultados de la optimización tras su finalización. Este tema todavía no está completada, y es necesario y se debe seguir desarrolándolo. Usando la librería para el diseño de las interfaces gráficas, se puede crear muchas soluciones interesantes y convenientes. Propongan sus ideas en los comentarios para el artículo: probablemente, en algún artículo siguiente, aparecerá una herramienta necesaria precisamente para su trabajo con los resultados de la optimización. 

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.

Nombre del archivo Comentario
MacdSampleMSFrames.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
FrameGenerator.mqh Clase para el trabajo con los resultados de la optimización

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

Archivos adjuntos |
Experts.zip (25.26 KB)
Random Decision Forest en el aprendizaje reforzado Random Decision Forest en el aprendizaje reforzado
Random Forest (RF) (en castellano, Bosques Aleatorios) con aplicación del bagging es uno de los métodos del aprendizaje automático más fuerte, que cede un poco ante el boosting del gradiente (Potenciación del gradiente). En este artículo, se realiza el intento de desarrollar un sistema comercial autoenseñable, que toma decisiones a base de la experiencia adquirida de la interacción con el mercado.
Sincronización de varios gráficos del mismo símbolo en timeframes diferentes Sincronización de varios gráficos del mismo símbolo en timeframes diferentes
Para tomar decisiones sobre la realización de las transacciones, a menudo es necesario analizar simultáneamente los gráficos en el proceso del trading. Además, los gráficos disponen de los objetos del análisis gráfico. Es bastante incómodo colocar los mismos objetos en todos los gráficos. En este artículo, yo propongo automatizar la clonación de los objetos en los gráficos.
Visualización de los resultados de la optimización según el criterio seleccionado Visualización de los resultados de la optimización según el criterio seleccionado
En este artículo, vamos a continuar el desarrollo de la aplicación MQL para el trabajo con los resultados de la optimización empezado en los artículos anteriores. Esta vez, mostraremos cómo se puede formar la tabla de los mejores resultados después de optimizar los parámetros indicando otro criterio a través de la interfaz gráfica.
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.