Visualizando la optimización de una estrategia comercial en MetaTrader 5

Anatoli Kazharski | 9 marzo, 2018


Contenido

Introducción

Al desarrollar algoritmos comerciales resulta útil analizar los resultados de las pruebas durante la optimización de parámetros. Sin embargo, para el concepto de efectividad del algoritmo comercial resulta insuficiente el gráfico en la pestaña Gráfico de optimización. Una cosa bastante distinta es ver simultáneamente las curvas de balance de multitud de pruebas directamente durante la optimización, y también tener la posibilidad de verlas incluso tras finalizar el proceso. En el artículo Visualizar una estrategia en el simulador de Meta Trader 5 ya se mostró una aplicación semejante. Desde entonces, han aparecido multitud de nuevas posibilidades. Por eso, ahora esta aplicación se puede implementar con una calidad diferente, muy superior.

En el artículo se muestra una aplicación MQL con interfaz gráfica para la visualización ampliada del proceso de optimización. La interfaz gráfica se crea con la ayuda de la última versión de la biblioteca EasyAndFast. En ocasiones, a muchos usarios les surge la siguiente pregunta: ¿para qué necesitamos las interfaces gráficas en las aplicaciones MQL? En este artículo se muestra en qué caso pueden resultar útil para los tráders. Asimismo, le vendrá bien a quienes ya usan esta biblioteca en sus desarrollos.

Desarrollo de la interfaz gráfica

Vamos a describir muy brevemente el proceso de creación de la interfaz gráfica de la aplicación. Pero si acaba de oír hablar por primera vez sobre la biblioteca EasyAndFast, podrá comprender rápidamente cómo utilizarla, y valorar lo fácil y sencillo que es ahora crear una interfaz gráfica para su aplicación MQL.

Bien, para comenzar, vamos a representar la estructura general de la aplicación desarrollada. En el archivo Program.mqh se contendrá la clase de la aplicación: CProgram. Esta clase básica debe estar vinculada con el motor gráfico de la biblioteca.

//+------------------------------------------------------------------+
//|                                                      Program.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- Clase de la biblioteca para crear la interfaz gráfica
#include <EasyAndFastGUI\WndEvents.mqh>
//+------------------------------------------------------------------+
//| Clase para crear la biblioteca                                   |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
  };

Para no saturar la imagen, en el esquema de la biblioteca EasyAndFast ha sido designada con un solo bloque (Library GUI). Podrá ver su esquema completo en la página de la biblioteca

 Fig. 1 – Inclusión de la biblioteca para crear GUI.

Fig. 1 Inclusión de la biblioteca para crear GUI.

Para vincular con las funciones del programa MQL en la clase CProgram debemos crear métodos análogos. Para trabajar con los frames de optimización, necesitaremos los métodos de la categoría OnTesterXXX().

class CProgram : public CWndEvents
  {
public:
   //--- Inicialización/desinicialización
   bool              OnInitEvent(void);
   void              OnDeinitEvent(const int reason);
   //--- Procesador del evento "nuevo tick"
   void              OnTickEvent(void);
   //--- Procesador del evento comercial
   void              OnTradeEvent(void);
   //--- Temporizador
   void              OnTimerEvent(void);
   //--- Simulador
   double            OnTesterEvent(void);
   void              OnTesterPassEvent(void);
   void              OnTesterInitEvent(void);
   void              OnTesterDeinitEvent(void);
  };

Entonces, en el archivo principal de la aplicación, todos estos métodos se deben llamar así:

//--- Inclusión de la clase de la aplicación
#include "Program.mqh"
CProgram program;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(void)
  {
//--- Inicialización del programa
   if(!program.OnInitEvent())
     {
      ::Print(__FUNCTION__," > Failed to initialize!");
      return(INIT_FAILED);
     }  
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) { program.OnDeinitEvent(reason); }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(void) { program.OnTickEvent(); }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer(void) { program.OnTimerEvent(); }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
   { program.ChartEvent(id,lparam,dparam,sparam); }
//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester(void) { return(program.OnTesterEvent()); }
//+------------------------------------------------------------------+
//| TesterInit function                                              |
//+------------------------------------------------------------------+
void OnTesterInit(void) { program.OnTesterInitEvent(); }
//+------------------------------------------------------------------+
//| TesterPass function                                              |
//+------------------------------------------------------------------+
void OnTesterPass(void) { program.OnTesterPassEvent(); }
//+------------------------------------------------------------------+
//| TesterDeinit function                                            |
//+------------------------------------------------------------------+
void OnTesterDeinit(void) { program.OnTesterDeinitEvent(); }
//+------------------------------------------------------------------+

En este aspecto, la plantilla de la aplicación está preparada para el desarrollo de la interfaz gráfica. El trabajo principal se realizará en la clase CProgram. Incluiremos todos los archivos necesarios para el trabajo en el archivo Program.mqh.

Ahora vamos a definir el contenido de la interfaz gráfica. Enumeraremos todos los elementos que debemos crear.

  • Formulario para los elementos de control.
  • Campo de edición para indicar el número de balances que se representarán en el gráfico.
  • Campo de edición para regular la velocidad de la nueva muestra de los resultados de la optimización.
  • Botón para iniciar la nueva muestra.
  • Recuadro para representar la estadística del resultado.
  • Recuadro para representar los parámetros externos del experto.
  • Gráficos para representar las curvas de balance.
  • Gráficos para representar todos los resultados de optimización.
  • Línea de estado para mostrar la información final adicional.
  • Indicador de ejecución, durante el nuevo desplazamiento representará el tanto por ciento de resultados mostrados del número total.

Aquí solo vamos a mostrar las declaraciones de los ejemplares de las clases de los elementos de gestión y los métodos para crearlos (ver la lista del código más abajo). El código de los propios métodos lo sacaremos a un archivo aparte CreateFrameModeGUI.mqh, que vincularemos al archivo de la clase CProgram. A medida que aumente el código de la aplicación desarrollada, este método de distribución por archivos aparte resultará bastante útil. Así será más sencillo orientarse en el proyecto.

class CProgram : public CWndEvents
  {
private:
   //--- Ventana
   CWindow           m_window1;
   //--- Línea de estado
   CStatusBar        m_status_bar;
   //--- Campo de edición
   CTextEdit         m_curves_total;
   CTextEdit         m_sleep_ms;
   //--- Botones
   CButton           m_reply_frames;
   //--- Recuadros
   CTable            m_table_stat;
   CTable            m_table_param;
   //--- Gráficos
   CGraph            m_graph1;
   CGraph            m_graph2;
   //--- Indicador de ejecución
   CProgressBar      m_progress_bar;
   //---
public:
   //--- Crea una interfaz gráfica para trabajar con frames en el modo de optimización
   bool              CreateFrameModeGUI(void);
   //---
private:
   //--- Formulario
   bool              CreateWindow(const string text);
   //--- Línea de estado
   bool              CreateStatusBar(const int x_gap,const int y_gap);
   //--- Recuadros
   bool              CreateTableStat(const int x_gap,const int y_gap);
   bool              CreateTableParam(const int x_gap,const int y_gap);
   //--- Campo 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);
   //--- Indicador de ejecución
   bool              CreateProgressBar(const int x_gap,const int y_gap,const string text);
  };
//+------------------------------------------------------------------+
//| Métodos para crear los elementos de gestión                      |
//+------------------------------------------------------------------+
#include "CreateFrameModeGUI.mqh"
//+------------------------------------------------------------------+

En el archivo CreateFrameModeGUI.mqh también anotaremos la inclusión del archivo con el que debemos tener vinculación. Como ejemplo, mostraremos aquí solo el método principal para crear la interfaz gráfica de la aplicación:

//+------------------------------------------------------------------+
//|                                           CreateFrameModeGUI.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Program.mqh"
//+---------------------------------------------------------------------------------+
//| Crea la interfaz gráfica                                                        |
//| para analizar los resultados de la optimización y el trabajo con los frames     |
//+---------------------------------------------------------------------------------+
bool CProgram::CreateFrameModeGUI(void)
  {
//--- Creamos esta interfaz solo en el modo de trabajo con los frames de optimización
   if(!::MQLInfoInteger(MQL_FRAME_MODE))
      return(false);
//--- Creamos el formulario para los elementos de gestión
   if(!CreateWindow("Frame mode"))
      return(false);
//--- Creamos los elementos de gestión
   if(!CreateStatusBar(1,23))
      return(false);
   if(!CreateCurvesTotal(7,25,"Curves total:"))
      return(false);
   if(!CreateSleep(145,25,"Sleep:"))
      return(false);
   if(!CreateReplyFrames(255,25,"Replay frames"))
      return(false);
   if(!CreateTableStat(2,50))
      return(false);
   if(!CreateTableParam(2,212))
      return(false);
   if(!CreateGraph1(200,50))
      return(false);
   if(!CreateGraph2(200,159))
      return(false);
//--- Indicador de ejecución
   if(!CreateProgressBar(2,3,"Processing..."))
      return(false);
//--- Finalizando la creación de GUI
   CWndEvents::CompletedGUI();
   return(true);
  }
...

En el esquema, esta conexión entre archivos que pertenecen a una misma clase, se designa con una flecha bilateral amarilla:

 Fig. 2 – Dividimos el proyecto en varios archivos.

Fig. 2.  Dividimos el proyecto en varios archivos.



Desarrollo de la clase para trabajar con los datos de los frames

Para trabajar con los frames de la optimización, escribiremos una clase aparte CFrameGenerator. La clase se contendrá en el archivo FrameGenerator.mqh, que se debe vincular al archivo Program.mqh. Como ejemplo, demostraremos dos variantes para obtener estos frames para la representación en los elementos de la interfaz gráfica. 

  • En el primer caso, para representar los frames en los objetos gráficos, se transmiten los punteros a estos objetos en los métodos de la clase.
  • En el segundo caso, obtendremos los datos de los frames para rellenar los recuadros de otras categorías con la ayuda de métodos especiales. 

Cada usario debe decidir por sí mismo cuál de estas variantes se dejará como principal.

En la biblioteca EasyAndFast , se usa la clase CGraphic de la biblioteca estándar para visualizar los datos. Para tener acceso a sus métodos, la conectaremos con el archivo FrameGenerator.mqh.

//+------------------------------------------------------------------+
//|                                               FrameGenerator.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include <Graphics\Graphic.mqh>
//+------------------------------------------------------------------+
//| Clase para obtener los resultados de la optimización             |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
  };

El esquema del programa ahora tiene el aspecto siguiente:

 Fig. 3 – Inclusión en el proyecto de las clases para el trabajo.

Fig. 3.  Inclusión en el proyecto de las clases para el trabajo.

Ahora vamos a analizar cómo se construye la clase CFrameGenerator. En ella también son necesarios los métodos para el procesamiento de los eventos del simulador de estrategias (ver la lista del código más abajo). Se llamarán en los métodos análogos de la clase de la aplicación que desarrollamos: CProgram. Al método CFrameGenerator::OnTesterInitEvent() se transmitirán los punteros a los objetos de los gráficos en los que se representa el proceso actual de optimización. 

  • En el primer gráfico (graph_balance) se representa el número indicado de las últimas series de balances de los resultados de la optimización.
  • En el segundo gráfico (graph_result) se representan los resultados totales de la optimización.
class CFrameGenerator
  {
private:
   //--- Punteros a los gráficos para la visualización de los datos
   CGraphic         *m_graph_balance;
   CGraphic         *m_graph_results;
   //---
public:
   //--- Procesador de eventos del simulador de estrategias
   void              OnTesterEvent(const double on_tester_value);
   void              OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_result);
   void              OnTesterDeinitEvent(void);
   
bool              OnTesterPassEvent(void);
  };
//+------------------------------------------------------------------+
//| Deberá llamarse en el procesador OnTesterInit()                  |
//+------------------------------------------------------------------+
void CFrameGenerator::OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_results)
  {
   m_graph_balance =graph_balance;
   m_graph_results =graph_results;
  }

En ambos gráficos se representarán los resultados positivos con color verde, y los resultados negativos, en rojo.

En el método CFrameGenerator::OnTesterEvent() obtenemos el balance del resultado de la prueba y los índices estadísticos. Estos datos se transmitirán en el frame con la ayuda de los métodos CFrameGenerator::GetBalanceData() y CFrameGenerator::GetStatData(). En el método CFrameGenerator::GetBalanceData() obtenemos toda la historia de la prueba y sumamos todas las transacciones in-/inout. El resultado obtenido se guarda paso a paso en la matriz m_balance[]. Esta matriz, a su vez, es miembro de la clase CFrameGenerator.

Al método CFrameGenerator::GetStatData() se transmite la matriz dinámica que después se enviará en el frame. Se establece el mismo tamaño para él que para la matriz del resultado del balance obtenido previamente, además se añade un número de elementos, en el que obtenemos algunos índices estadísticos.

//--- Número de índices estadísticos
#define STAT_TOTAL 7
//+------------------------------------------------------------------+
//| Clase para el trabajo con los resultados de la optimización      |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   //--- Balance del resultado
   double            m_balance[];
   //---
private:
   //--- Obtiene los datos del balance
   int               GetBalanceData(void);
   //--- Obtiene los datos estadísticos
   void              GetStatData(double &dst_array[],double on_tester_value);
  };
//+------------------------------------------------------------------+
//| Obtiene los datos del balance                                    |
//+------------------------------------------------------------------+
int CFrameGenerator::GetBalanceData(void)
  {
   int    data_count      =0;
   double balance_current =0;
//--- Solicitamos la historia comercial completa
   ::HistorySelect(0,LONG_MAX);
   uint deals_total=::HistoryDealsTotal();
//--- Reunimos los datos sobre las transacciones
   for(uint i=0; i<deals_total; i++)
     {
      //--- Obtenemos el ticket
      ulong ticket=::HistoryDealGetTicket(i);
      if(ticket<1)
         continue;
      //--- Si el balance inicial o la transacción out-/inout
      long entry=::HistoryDealGetInteger(ticket,DEAL_ENTRY);
      if(i==0 || entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT)
        {
         double swap      =::HistoryDealGetDouble(ticket,DEAL_SWAP);
         double profit    =::HistoryDealGetDouble(ticket,DEAL_PROFIT);
         double commision =::HistoryDealGetDouble(ticket,DEAL_COMMISSION);
         //--- Cálculo del balance
         balance_current+=(profit+swap+commision);
         //--- Guardar en la matriz
         data_count++;
         ::ArrayResize(m_balance,data_count,100000);
         m_balance[data_count-1]=balance_current;
        }
     }
//--- Retornar el número de datos
   return(data_count);
  }
//+------------------------------------------------------------------+
//| Obtiene los datos estadísticos                                   |
//+------------------------------------------------------------------+
void CFrameGenerator::GetStatData(double &dst_array[],double on_tester_value)
  {
   ::ArrayResize(dst_array,::ArraySize(m_balance)+STAT_TOTAL);
   ::ArrayCopy(dst_array,m_balance,STAT_TOTAL,0);
//--- Rellenamos los primeros valores de la matriz (STAT_TOTAL) con los resultados de la simulación
   dst_array[0] =::TesterStatistics(STAT_PROFIT);               // beneficio neto
   dst_array[1] =::TesterStatistics(STAT_PROFIT_FACTOR);        // factor de beneficio
   dst_array[2] =::TesterStatistics(STAT_RECOVERY_FACTOR);      // factor de recuperación
   dst_array[3] =::TesterStatistics(STAT_TRADES);               // número de trades
   dst_array[4] =::TesterStatistics(STAT_DEALS);                // número de transacciones
   dst_array[5] =::TesterStatistics(STAT_EQUITY_DDREL_PERCENT); // reducción máxima de los fondos en tanto por ciento
   dst_array[6] =on_tester_value;                               // valor del criterio de optimización personalizado
  }

Los métodos CFrameGenerator::GetBalanceData() y CFrameGenerator::GetStatData() se llaman en el procesador de eventos de finalización de la simulación, CFrameGenerator::OnTesterEvent(). Los datos han sido obtenidos. Los enviamos en el frame al terminal

//+---------------------------------------------------------------------------+
//| Prepara la matriz de los valores del balance y la envía en el frame       |
//| La función deberá llamarse en el experto en el procesador  OnTester()     |
//+---------------------------------------------------------------------------+
void CFrameGenerator::OnTesterEvent(const double on_tester_value)
  {
//--- Obtenemos los datos del balance
   int data_count=GetBalanceData();
//--- Matriz para el envío de datos en el frame
   double stat_data[];
   GetStatData(stat_data,on_tester_value);
//--- Creamos un frame con los datos y lo enviamos al terminal
   if(!::FrameAdd(::MQLInfoString(MQL_PROGRAM_NAME),1,data_count,stat_data))
      ::Print(__FUNCTION__," > Frame add error: ",::GetLastError());
   else
      ::Print(__FUNCTION__," > Frame added, Ok");
  }

Ahora vamos a analizar los métodos que se usarán en el procesador de eventos de llegada de frames durante la optimización: CFrameGenerator::OnTesterPassEvent(). Necesitaremos las variables para trabajar con los frames: nombre, identificador, número de llegada, valor adoptado y matriz de datos utilizada. Todos estos datos se envían al frame con la ayuda de la función FrameAdd(), mostrada más arriba.

class CFrameGenerator
  {
private:
   //--- Variables para trabajar con frames
   string            m_name;
   ulong             m_pass;
   long              m_id;
   double            m_value;
   double            m_data[];
  };

En el método CFrameGenerator::SaveStatData() de la matriz que hemos adoptado en el frame, reuniremos los índices estadísticos y los guardaremos en una matriz de línea aparte. Aquí los datos contendrán el nombre del índice y su valor. Como separador se usará el símbolo '='.

class CFrameGenerator
  {
private:
   //--- Matriz con los índices estadísticos
   string            m_stat_data[];
   //---
private:
   //--- Guardar los datos estadísticos 
   void              SaveStatData(void);
  };
//+------------------------------------------------------------------+
//| Guarda los índices estadísticos del resultado en una matriz      |
//+------------------------------------------------------------------+
void CFrameGenerator::SaveStatData(void)
  {
//--- Matriz para la adopción de los índices estadísticos del frame
   double stat[];
   ::ArrayCopy(stat,m_data,0,0,STAT_TOTAL);
   ::ArrayResize(m_stat_data,STAT_TOTAL);
//--- Rellenamos la matriz con los resultados de la simulación
   m_stat_data[0] ="Net profit="+::StringFormat("%.2f",stat[0]);
   m_stat_data[1] ="Profit Factor="+::StringFormat("%.2f",stat[1]);
   m_stat_data[2] ="Factor Recovery="+::StringFormat("%.2f",stat[2]);
   m_stat_data[3] ="Trades="+::StringFormat("%G",stat[3]);
   m_stat_data[4] ="Deals="+::StringFormat("%G",stat[4]);
   m_stat_data[5] ="Equity DD="+::StringFormat("%.2f%%",stat[5]);
   m_stat_data[6] ="OnTester()="+::StringFormat("%G",stat[6]);
  }

Los datos estadísticos se deben guardar en una matriz aparte, para que después podamos obtenerlos en la clase de la aplicación (CProgram) y rellenar el recuadro. Para obtenerlos, se llama el método público CFrameGenerator::CopyStatData(), transmitiendo la matriz para el copiado.

class CFrameGenerator
  {
public:
   //--- Retorna los índices estadísticos a la matriz transmitida
   int               CopyStatData(string &dst_array[]) { return(::ArrayCopy(dst_array,m_stat_data)); }
  };

Para actualizar los gráficos de los resultados durante la optimización, necesitaremos métodos auxiliares, responsables de la adición a las matrices de los resultados positivos y negativos. Preste atención, en el eje Х, el resultado se añade según el valor actual del contador de frames. En conclusión, los vacíos formados no se representarán en el gráfico, como valores cero.

//--- Tamaño de reserva para las matrices
#define RESERVE_FRAMES 1000000
//+------------------------------------------------------------------+
//| Clase para el trabajo con los resultados de la optimización      |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   //--- Contador de frames
   ulong             m_frames_counter;
   //--- Datos de los resultados positivos y negativos
   double            m_loss_x[];
   double            m_loss_y[];
   double            m_profit_x[];
   double            m_profit_y[];
   //---
private:
   //--- Añade un resultado negativo (1) y positivo (2) a las matrices
   void              AddLoss(const double loss);
   void              AddProfit(const double profit);
  };
//+------------------------------------------------------------------+
//| Añade un resultado negativo a la matriz                          |
//+------------------------------------------------------------------+
void CFrameGenerator::AddLoss(const double loss)
  {
   int size=::ArraySize(m_loss_y);
   ::ArrayResize(m_loss_y,size+1,RESERVE_FRAMES);
   ::ArrayResize(m_loss_x,size+1,RESERVE_FRAMES);
   m_loss_y[size] =loss;
   m_loss_x[size] =(double)m_frames_counter;
  }
//+------------------------------------------------------------------+
//| Añade un resultado positivo a la matriz                          |
//+------------------------------------------------------------------+
void CFrameGenerator::AddProfit(const double profit)
  {
   int size=::ArraySize(m_profit_y);
   ::ArrayResize(m_profit_y,size+1,RESERVE_FRAMES);
   ::ArrayResize(m_profit_x,size+1,RESERVE_FRAMES);
   m_profit_y[size] =profit;
   m_profit_x[size] =(double)m_frames_counter;
  }

Aquí los métodos principales de actualización de los gráficos son CFrameGenerator::UpdateResultsGraph() y CFrameGenerator::UpdateBalanceGraph():

class CFrameGenerator
  {
private:
   //--- Actualizar el gráfico de resultados
   void              UpdateResultsGraph(void);
   //--- Actualizar el gráfico de balances
   void              UpdateBalanceGraph(void);
  };

En el método CFrameGenerator::UpdateResultsGraph() los resultados de las pruebas (beneficio positivo/negativo) se añaden a la matriz. A continuación, estos datos se representan en el gráfico correspondiente. En los nombres de las series de este gráfico mostraremos el número de resultados positivos y negativos. 

//+------------------------------------------------------------------+
//| Actualizar el gráfico de resultados                              |
//+------------------------------------------------------------------+
void CFrameGenerator::UpdateResultsGraph(void)
  {
//--- Resultado negativo
   if(m_data[0]<0)
      AddLoss(m_data[0]);
//--- Resultado positivo
   else
      AddProfit(m_data[0]);
//--- Actualizar las series en el gráfico de resultados de optimización
   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();
  }

Al mismo comienzo del método CFrameGenerator::UpdateBalanceGraph() de la matriz de datos transmitidos en el frame, se extraen aquellos que se relacionan con el balance. Puesto que en el gráfico se pueden representar simultáneamente varias series, haremos que la actualización de las series sea secuencial. Para ello, usaremos un contador de series aparte. Para ajustar el número de series de balances representadas simultáneamente en el gráfico, necesitaremos el método público CFrameGenerator::SetCurvesTotal(). En cuanto el contador de series en él llega hasta el límite establecido, la cuenta comienza de nuevo. Como nombre de las series, actuará el contador de frames. El color de las series dependerá del resultado: verde en caso positivo, rojo, en caso negativo.

Puesto que el número de transacciones en cada resultado es diferente, para que en el gráfico quepan todas las series necesarias, deberemos determinar la máxima, y ya según ella, establecer el máximo en el eje X.

class CFrameGenerator
  {
private:
   //--- Número de series
   uint              m_curves_total;
   //--- Índice de la serie actual en el gráfico
   uint              m_last_serie_index;
   //--- Para definir la serie máxima
   double            m_curve_max[];
   //---
public:
   //--- Establecer el número de series para la representación en el gráfico
   void              SetCurvesTotal(const uint total);
  };
//+-------------------------------------------------------------------------+
//| Establecer el número de series para la representación en el gráfico     |
//+-------------------------------------------------------------------------+
void CFrameGenerator::SetCurvesTotal(const uint total)
  {
   m_curves_total=total;
   ::ArrayResize(m_curve_max,total);
   ::ArrayInitialize(m_curve_max,0);
  }
//+------------------------------------------------------------------+
//| Actualizar el gráfico de balances                                |
//+------------------------------------------------------------------+
void CFrameGenerator::UpdateBalanceGraph(void)
  {
//--- Matriz para adoptar los valores del balance del frame actual
   double serie[];
   ::ArrayCopy(serie,m_data,0,STAT_TOTAL,::ArraySize(m_data)-STAT_TOTAL);
//--- Enviamos la matrices para mostrar en el gráfico de balance
   CCurve *curve=m_graph_balance.CurveGetByIndex(m_last_serie_index);
   curve.Name((string)m_frames_counter);
   curve.Color((m_data[0]>=0)? ::ColorToARGB(clrLimeGreen) : ::ColorToARGB(clrRed));
   curve.Update(serie);
//--- Obtenemos el tamaño de la serie
   int serie_size=::ArraySize(serie);
   m_curve_max[m_last_serie_index]=serie_size;
//--- Definimos la serie con el número máximo de elementos
   double x_max=0;
   for(uint i=0; i<m_curves_total; i++)
      x_max=::fmax(x_max,m_curve_max[i]);
//--- Propiedades del eje horizontal
   CAxis *x_axis=m_graph_balance.XAxis();
   x_axis.Min(0);
   x_axis.Max(x_max);
   x_axis.DefaultStep((int)(x_max/8.0));
//--- Actualizar el gráfico
   m_graph_balance.CalculateMaxMinValues();
   m_graph_balance.CurvePlotAll();
   m_graph_balance.Update();
//--- Aumentamos el contador de series
   m_last_serie_index++;
//--- Si hemos llegado al límite, reseteamos el contador de series
   if(m_last_serie_index>=m_curves_total)
      m_last_serie_index=0;
  }

Bien, hemos analizados los métodos necesarios para organizar el trabajo en el procesador de eventos. Ahora vamos a analizar cómo se construye el propio método-procesador CFrameGenerator::OnTesterPassEvent(). Retorna true, mientras se desarrolla el proceso de optimización y la función FrameNext() obtiene los datos de los frames. Al finalizar la optimización, el método retorna false.

En la lista de parámetros del experto que podemos obtener con la ayuda de la función FrameInputs(), primero van los parámetros para la optimización, y ya después los que no participan en la optimización. 

Si los datos del frame se han obtenido, con la ayuda de la funciónFrameInputs() obtenemos los parámetros del experto en la pasada de optimizaición actual. A continuación, guardamos los índices estadísticos, actualizamos los gráficos y aumentamos el contador de frames. Después de ello, el método CFrameGenerator::OnTesterPassEvent() retorna true antes de la siguiente llamada.

class CFrameGenerator
  {
private:
   //--- Parámetros del experto
   string            m_param_data[];
   uint              m_par_count;
  };
//+-----------------------------------------------------------------------------------------+
//| Obtiene el frame con los datos al realizar la optimización y representa el gráfico      |
//+-----------------------------------------------------------------------------------------+
bool CFrameGenerator::OnTesterPassEvent(void)
  {
//--- Al obtener un nuevo frame, intentamos obtener de él los datos
   if(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
      //--- Obtenemos los parámetros de entrada del experto, para los cuales se ha formado el frame
      ::FrameInputs(m_pass,m_param_data,m_par_count);
      //--- Guardamos los índices estadísticos del resultado en la matriz
      SaveStatData();
      //--- Actualizamos el gráfico de resultados y balances
      UpdateResultsGraph();
      UpdateBalanceGraph();
      //--- Aumentamos el contador de frames procesados
      m_frames_counter++;
      return(true);
     }
//---
   return(false);
  }

Al terminar la optimización en el modo de procesamiento de frames se genera el evento TesterDeinit y se llama el método CFrameGenerator::OnTesterDeinitEvent(). En el momento actual no todos los frames se pueden procesar durante la optimización, por eso el gráfico de visualización de resultados estará incompleto. Para ver la imagen completa, deberemos iterar justo después de la optimización en el ciclo por todos los frames con el método CFrameGenerator::FinalRecalculateFrames() y actualizar de nuevo el gráfico.

Para ello, primero tenemos que trasladar el puntero al inicio de la lista de frames, y después resetear las matrices de los resultados y el contador de frames. A continuación, pasamos por la lista completa de frames, rellenamos las matrices con los resultados positivos y negativos, y al final actualizamos el gráfico.

class CFrameGenerator
  {
private:
   //--- Liberamos las matrices
   void              ArraysFree(void);
   //--- Recálculo final de los datos de todos los frames después de la optimización
   void              FinalRecalculateFrames(void);
  };
//+------------------------------------------------------------------+
//| Liberamos las matrices                                           |
//+------------------------------------------------------------------+
void CFrameGenerator::ArraysFree(void)
  {
   ::ArrayFree(m_loss_y);
   ::ArrayFree(m_loss_x);
   ::ArrayFree(m_profit_y);
   ::ArrayFree(m_profit_x);
  }
//+----------------------------------------------------------------------------------+
//| Recálculo final de los datos de todos los frames después de la optimización      |
//+----------------------------------------------------------------------------------+
void CFrameGenerator::FinalRecalculateFrames(void)
  {
//--- Trasladamos el puntero de los frames al principio
   ::FrameFirst();
//--- Reseteamos el contador y las matrices
   ArraysFree();
   m_frames_counter=0;
//--- Iniciamos la iteración de frames
   while(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
      //--- Resultado negativo
      if(m_data[0]<0)
         AddLoss(m_data[0]);
      //--- Resultado positivo
      else
         AddProfit(m_data[0]);
      //--- Aumentamos el contador de frames procesados
      m_frames_counter++;
     }
//--- 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();
  }

Entonces el código del método CFrameGenerator::OnTesterDeinitEvent() será igual que el mostrado más abajo. Aquí debemos recordar el número total de frames y también resetear el contador.

//+------------------------------------------------------------------+
//| Deberá llamarse en el procesador OnTesterDeinit()                |
//+------------------------------------------------------------------+
void CFrameGenerator::OnTesterDeinitEvent(void)
  {
//--- Recálculo final de los datos de todos los frames después de la optimización
   FinalRecalculateFrames();
//--- Recordamos el número total de frames y reseteamos los contadores
   m_frames_total     =m_frames_counter;
   m_frames_counter   =0;
   m_last_serie_index =0;
  }

A continuación, analizamos cómo usar los métodos de la clase CFrameGenerator en la clase de la aplicación. 

Trabajando con los datos de optimización en la clase de la aplicación

Crearemos la interfaz gráfica de nuestra aplicación en el método de inicialización de la simulación CProgram::OnTesterInitEvent(). Después de crearla, es necesario hacer la interfaz gráfica no disponible. Para ello, necesitaremos los métodos adicionales CProgram::IsAvailableGUI() y CProgram::IsLockedGUI(), que se usarán también en los otros métodos de la clase CProgram.

Inicializamos el generador de frames: para ello, transmitiremos los punteros a los gráficos en los que se visualizarán los resultados de la optimización.

class CProgram : public CWndEvents
  {
private:
   //--- Disponibilidad de la interfaz
   void              IsAvailableGUI(const bool state);
   void              IsLockedGUI(const bool state);
  }
//+------------------------------------------------------------------+
//| Evento de inicio del proceso de optimización                     |
//+------------------------------------------------------------------+
void CProgram::OnTesterInitEvent(void)
  {
//--- Creamos la interfaz gráfica
   if(!CreateFrameModeGUI())
     {
      ::Print(__FUNCTION__," > Could not create the GUI!");
      return;
     }
//--- Hacemos la interfaz disponible
   IsLockedGUI(false);
//--- Inicializamos el generador de frames
   m_frame_gen.OnTesterInitEvent(m_graph1.GetGraphicPointer(),m_graph2.GetGraphicPointer());
  }
//+------------------------------------------------------------------+
//| Disponibilidad de la interfaz                                    |
//+------------------------------------------------------------------+
void CProgram::IsAvailableGUI(const bool state)
  {
   m_window1.IsAvailable(state);
   m_sleep_ms.IsAvailable(state);
   m_curves_total.IsAvailable(state);
   m_reply_frames.IsAvailable(state);
  }
//+------------------------------------------------------------------+
//| Bloqueo de la interfaz                                           |
//+------------------------------------------------------------------+
void CProgram::IsLockedGUI(const bool state)
  {
   m_window1.IsAvailable(state);
   m_sleep_ms.IsLocked(!state);
   m_curves_total.IsLocked(!state);
   m_reply_frames.IsLocked(!state);
  }

Más arriba hemos hablado de que los datos en los recuadros los vamos a actualizar en la clase de la aplicación con la ayuda de los métodos CProgram::UpdateStatTable() y CProgram::UpdateParamTable(). El código de ambos recuadros es idéntico, por eso mostraremos para el ejemplo solo uno de ellos. Los nombres de los índices/parámetros y sus valores se representan en una línea con la ayuda del separador ‘=’. Por eso, en el ciclo pasamos por ellos, los dividimos en dos elementos y los introducimos en una matriz aparte. Después, incorporamos estos valores a las celdas del recuadro.

class CProgram : public CWndEvents
  {
private:
   //--- Actualizamos el recuadro estadístico
   void              UpdateStatTable(void);
   //--- Actualizando el recuadro de los parámetros
   void              UpdateParamTable(void);
  }
//+------------------------------------------------------------------+
//| Actualizamos el recuadro estadístico                             |
//+------------------------------------------------------------------+
void CProgram::UpdateStatTable(void)
  {
//--- Obtenemos la matriz de datos para el recuadro estadístico
   string stat_data[];
   int total=m_frame_gen.CopyStatData(stat_data);
   for(int i=0; i<total; i++)
     {
      //--- Dividimos en dos líneas e introducimos en el recuadro
      string array[];
      if(::StringSplit(stat_data[i],'=',array)==2)
        {
         if(m_frame_gen.CurrentFrame()>1)
            m_table_stat.SetValue(1,i,array[1],0,true);
         else
           {
            m_table_stat.SetValue(0,i,array[0],0,true);
            m_table_stat.SetValue(1,i,array[1],0,true);
           }
        }
     }
//--- Actualizamos el recuadro
   m_table_stat.Update();
  }

Ambos métodos para la actualización de datos en el recuadro se llaman en el método CProgram::OnTesterPassEvent() conforme a la respuesta positiva del método homónimo CFrameGenerator::OnTesterPassEvent():

//+------------------------------------------------------------------+
//| Evento de procesamiento de la pasada de optimización             |
//+------------------------------------------------------------------+
void CProgram::OnTesterPassEvent(void)
  {
//--- Procesamos los resultados de optimización obtenidos y mostramos el gráfico
   if(m_frame_gen.OnTesterPassEvent())
     {
      UpdateStatTable();
      UpdateParamTable();
     }
  }

Al finalizar la optimización, el método CProgram::CalculateProfitsAndLosses() calcula el tanto por ciento de resultados positivos y negativos y muestra esta información en la línea de estado:

class CProgram : public CWndEvents
  {
private:
   //--- Calculamos la proporción de resultados positivos y negativos
   void              CalculateProfitsAndLosses(void);
  }
//+------------------------------------------------------------------+
//| Calculamos la proporcioón de resultados positivos y negativos    |
//+------------------------------------------------------------------+
void CProgram::CalculateProfitsAndLosses(void)
  {
//--- Salimos si no hay frames
   if(m_frame_gen.FramesTotal()<1)
      return;
//--- Número de resultados positivos y negativos
   int losses  =m_frame_gen.LossesTotal();
   int profits =m_frame_gen.ProfitsTotal();
//--- Tanto por ciento
   string pl =::DoubleToString(((double)losses/(double)m_frame_gen.FramesTotal())*100,2);
   string pp =::DoubleToString(((double)profits/(double)m_frame_gen.FramesTotal())*100,2);;
//--- Mostramos en la línea de estado
   m_status_bar.SetValue(1,"Profits: "+(string)profits+" ("+pp+"%)"+" / Losses: "+(string)losses+" ("+pl+"%)");
   m_status_bar.GetItemPointer(1).Update(true);
  }

Más abajo mostramos el código del método para el procesamiento del evento TesterDeinit. La inicialización del núcleo gráfico indica que se monitoreará el desplazamiento el cursor del ratón y se activará el temporizador. Por desgracia, en la versión actual de MetaTrader 5 el temporizador no se activa al final de la optimización. Esperaremos que esta posibilidad aparezca en el futuro.

//+------------------------------------------------------------------+
//| Evento de finalización del proceso de optimización               |
//+------------------------------------------------------------------+
void CProgram::OnTesterDeinitEvent(void)
  {
//--- Finalización de la optimización
   m_frame_gen.OnTesterDeinitEvent();
//--- Hacemos la interfaz disponible
   IsLockedGUI(true);
//--- Calculamos la proporción de resultados positivos y negativos
   CalculateProfitsAndLosses();
//--- Inicializamos el núcleo GUI
   CWndEvents::InitializeCore();
  }

Ahora también podemos trabajar con los datos de los frames al finalizar la optimización. El experto se encuentra en el gráfico en el terminal, y existe acceso a los frames para el análisis de resultados. La interfaz gráfica hace esto de forma intuitiva y comprensible. En el método-procesador de los eventos CProgram::OnEvent() monitorearemos:

  • el cambio de valor en el campo de edición para establecer el número de series de balances representadas en el gráfico;
  • el inicio de la visualización de los resultados de optimización.

Para actualizar el gráfico después de cambiar el número de las series se usa el método CProgram::UpdateBalanceGraph(). Aquí establecemos el número de series para trabajar en el generador de frames, y después reservamos el mismo número en el gráfico.

class CProgram : public CWndEvents
  {
private:
   //--- Actualizamos el gráfico
   void              UpdateBalanceGraph(void);
  };
//+------------------------------------------------------------------+
//| Actualizamos el gráfico                                          |
//+------------------------------------------------------------------+
void CProgram::UpdateBalanceGraph(void)
  {
//--- Establecemos el número de series para el trabajo
   int curves_total=(int)m_curves_total.GetValue();
   m_frame_gen.SetCurvesTotal(curves_total);
//--- Eliminamos la serie
   CGraphic *graph=m_graph1.GetGraphicPointer();
   int total=graph.CurvesTotal();
   for(int i=total-1; i>=0; i--)
      graph.CurveRemoveByIndex(i);
//--- Añadir serie
   double data[];
   for(int i=0; i<curves_total; i++)
      graph.CurveAdd(data,CURVE_LINES,"");
//--- Actualizar el gráfico
   graph.CurvePlotAll();
   graph.Update();
  }

En el procesador de eventos, el método CProgram::UpdateBalanceGraph() se llama en el caso de que se conmuten los botones en el campo de edición (ON_CLICK_BUTTON) y en el caso de que se introduzca el valor en el campo de edición con el teclado (ON_END_EDIT):

//+------------------------------------------------------------------+
//| Procesador de eventos                                            |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Eventos de pulsación de botones
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- Cambiamos el número de series en el gráfico
      if(lparam==m_curves_total.Id())
        {
         UpdateBalanceGraph();
         return;
        }
      return;
     }
//--- Eventos de introducción de valores en el campo de edición
   if(id==CHARTEVENT_CUSTOM+ON_END_EDIT)
     {
      //--- Cambiamos el número de series en el gráfico
      if(lparam==m_curves_total.Id())
        {
         UpdateBalanceGraph();
         return;
        }
      return;
     }
  }

Para visualizar los resultados, después de la optimización en la clase CFrameGenerator se implementa el método público CFrameGenerator::ReplayFrames(). Aquí, al mismo inicio, definimos con el contador de frames: si el proceso se acaba de iniciar, las matrices se resetean y el puntero de frames se traslada al mismo inicio de la lista. A continuación, se inicia la iteración de los frames y se realizan las mismas acciones que en el método CFrameGenerator::OnTesterPassEvent(), anteriormente descrito. Si obtenemos el frame, el método retorna true. Al finalizar del proceso, los contadores de frames y las series se resetean, y el método retorna false

class CFrameGenerator
  {
public:
   //--- Iteración de frames
   bool              ReplayFrames(void);
  };
//+-----------------------------------------------------------------------------+
//| Nueva reproducción de los frames después de finalizar la optimización       |
//+-----------------------------------------------------------------------------+
bool CFrameGenerator::ReplayFrames(void)
  {
//--- Trasladamos el puntero de los frames al principio
   if(m_frames_counter<1)
     {
      ArraysFree();
      ::FrameFirst();
     }
//--- Iniciamos la iteración de frames
   if(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
      //--- Obtenemos los parámetros de entrada del experto, para los cuales se ha formado el frame
      ::FrameInputs(m_pass,m_param_data,m_par_count);
      //--- Guardamos los índices estadísticos del resultado en la matriz
      SaveStatData();
      //--- Actualizamos el gráfico de resultados y balances
      UpdateResultsGraph();
      UpdateBalanceGraph();
      //--- Aumentamos el contador de frames procesados
      m_frames_counter++;
      return(true);
     }
//--- Finalizamos la iteración
   m_frames_counter   =0;
   m_last_serie_index =0;
   return(false);
  }

El método CFrameGenerator::ReplayFrames() se llama en la clase CProgram del método ViewOptimizationResults(). Antes de iniciar la reproducción de los frames, la interfaz gráfica se hace inaccesible. La velocidad de desplazamiento se puede regular indicando la pausa en el campo de edición Sleep. En este momento, en la línea de estado se mostrará la barra de progreso para definir cuánto tiempo queda hasta el final del proceso.

class CFrameGenerator
  {
private:
   //--- Visualizamos los resultados de la optimización
   void              ViewOptimizationResults(void);
  };
//+------------------------------------------------------------------+
//| Visualizamos los resultados de la optimización                   |
//+------------------------------------------------------------------+
void CProgram::ViewOptimizationResults(void)
  {
//--- Hacemos la interfaz disponible
   IsAvailableGUI(false);
//--- Pausa
   int pause=(int)m_sleep_ms.GetValue();
//--- Iniciamos la reproducción de los frames
   while(m_frame_gen.ReplayFrames() && !::IsStopped())
     {
      //--- Actualizamos el recuadro
      UpdateStatTable();
      UpdateParamTable();
      //--- Actualizamos la barra de progreso
      m_progress_bar.Show();
      m_progress_bar.LabelText("Replay frames: "+string(m_frame_gen.CurrentFrame())+"/"+string(m_frame_gen.FramesTotal()));
      m_progress_bar.Update((int)m_frame_gen.CurrentFrame(),(int)m_frame_gen.FramesTotal());
      //--- Pausa
      ::Sleep(pause);
     }
//--- Calculamos la proporción de resultados positivos y negativos
   CalculateProfitsAndLosses();
//--- Ocultar la barra de progreso
   m_progress_bar.Hide();
//--- Hacemos la interfaz disponible
   IsAvailableGUI(true);
   m_reply_frames.MouseFocus(false);
   m_reply_frames.Update(true);
  }

La llamada del método CProgram::ViewOptimizationResults() tiene lugar al pulsar el botón Replay frames en la interfaz gráfica de la aplicación. Se genera el evento ON_CLICK_BUTTON.

//+------------------------------------------------------------------+
//| Procesador de eventos                                            |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Eventos de pulsación de botones
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- Visualizamos los resultados de la optimización 
      if(lparam==m_reply_frames.Id())
        {
         ViewOptimizationResults();
         return;
        }
      //--- 
      ...
      return;
     }
  }

En el siguiente apartado del artículo analizaremos qué resultado final obtendremos, y qué verá el usuario en el gráfico durante la optimización en el modo de trabajo con frames.


Demostrando el resultado obtenido

Para realizar las pruebas, utilizaremos un algortimo comercial del paquete estándar: Moving Average. Lo formalizamos como clase tal cual, sin adiciones ni correcciones. Todos los archivos de la aplicación desarrollada se ubicarán en una carpeta. El archivo con la estrategia lo incluimos en el archivo Program.mqh.

Como complemento aquí se incluye el archivo FormatString.mqh con las funciones para formatear las líneas. Por el momento, no son parte de ninguna clase, por eso designaremos la flecha con color negro. Como conclusión, el esquema de la aplicación tendrá el aspecto siguiente:

Fig. 4 – Incluimos la clase con la estrategia comercial y el archivo con las funciones adicionales. 

Fig. 4. Incluimos la clase con la estrategia comercial y el archivo con las funciones adicionales.

Vamos a intentar optimizar los parámetros y ver qué aspecto tienen en el gráfico en el terminal. Ajustes del simulador: símbolo EURUSD, marco temporal H1, rango temporal 2017.01.01 – 2018.01.01.

Fig. 5 – Demostración del resultado del experto Moving Average del paquete estándar.

Fig. 5.  Demostración del resultado del experto Moving Average del paquete estándar.

Como vemos, ha resultado bastante informativo. Casi todos los resultados de este algoritmo comercial son negativos (95.23%). Si aumentamos el rango temporal, los resultados serán aún peores. Pero sabemos que, a la hora de desarrollar un sistema comercial, debemos intentar que la mayoría de los resultados sean positivos. De lo contrario, el algoritmo dará pérdidas, y no será recomendable utilizarlo. Hay que optimizar los parámetros con la mayor cantidad de datos posible e intentar que las transacciones sean cuantas más, mejor.  

Vamos a intentar poner a prueba otro algoritmo comercial del paquete estándar, MACD Sample.mq5. Ya se ha formalizado como clase. Después de pulir algunos detalles, podremos incluirla simplemente en nuestra aplicación, como la anterior. Vamos a ponerlo a prueba con el mismo símbolo y marco temporal, pero aumentaremos el rango temporal para incrementar el número de transacciones en los tests (2010.01.01 – 2018.01.01). Aquí tenemos el resultado de la optimización del experto comercial:

 Fig. 6 – Demostración del resultado del experto MACD Sample del paquete estándar.

Fig. 6.  Demostración del resultado de optimización del experto MACD Sample.

Aquí vemos otro resultado completamente distinto: 90,89% de resultados positivos.

La optimización de los parámetros puede ocupar mucho tiempo, dependiendo del volumen de datos utilizados. No es obligatorio estar sentado todo el tiempo ante el monitor durante este proceso. Después de optimizar, podemos iniciar una nueva visualización de los resultados en el modo rápido, pulsando el botón Replay frames. Vamos a iniciar el proceso de reproducción de frames, estableciendo para la muestra un límite de 25 series. Este es el aspecto que tiene:

Fig. 7 - Demostración del resultado del experto MACD Sample tras la optimización.

Fig. 7. Demostración del resultado del experto MACD Sample tras la optimización.


Conclusión

En el artículo se muestra una versión actual del programa, diseñada para obtener y analizar los frames de optimización. Los datos se visualizan en el entorno de una interfaz gráfica creada sobre la base de la biblioteca EasyAndFast

Una de las desventajas, o mejor dicho, uno de los defectos de esta solución es el hecho de que tras finalizar la optimización en el modo de procesamiento de frames es imposible iniciar el temporizador. Esto genera ciertas limitaciones en el trabajo con la propia interfaz gráfica. El segundo problema es que al eliminar el experto del gráfico no se activa la desinicialización en la función OnDeinit(), y esto dificulta el procesamiento correcto de este evento. Es posible que en uno de los siguientes builds de MetaTrader 5 estos problemas sean resueltos.