Visualizando la optimización de una estrategia comercial en MetaTrader 5
Anatoli Kazharski | 9 marzo, 2018
Contenido
- Introducción
- Desarrollo de la interfaz gráfica
- Desarrollo de la clase para trabajar con los datos de los frames
- Trabajando con los datos de optimización en la clase de la aplicación
- Demostrando el resultado obtenido
- Conclusión
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.
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.
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.
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.
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.
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 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.
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.