Trabajando con los resultados de la optimización mediante la interfaz gráfica
Contenido
- Introducción
- Desarrollo de la interfaz gráfica
- Guardando los resultados de optimización
- Extracción de datos del frame
- Visualización de datos e interacción con la interfaz gráfica
- Conclusión
Introducción
Continuamos desarrollar el tema del procesamiento y el análisis de los resultados de la optimización. En el artículo anterior hemos demostrado cómo visualizar los resultados de la optimización mediante la interfaz gráfica de la aplicación MQL5. Ahora vamos a complicar nuestra tarea: seleccionamos 100 mejores resultados de la optimización y los mostramos en la tabla de la interfaz gráfica.
Además de eso, continuamos el desarrollo del tema de los gráficos del balance de multisímbolos, que también ha sido presentado en el artículo correspondiente. Vamos a combinar las ideas de estos dos artículos, y hagamos que el usuario obtenga el gráfico del balance de multisímbolos y de la reducción (drawdown) en gráficos separados seleccionando una fila de la tabla de los resultados de la optimización. Así, después de la optimización de los parámetros del Asesor Experto (EA), el trader podrá analizar y seleccionar los resultados que le interesan para el trabajo más rápidamente.
Desarrollo de la interfaz gráfica
La interfaz gráfica del EA de prueba va a incluir los siguientes elementos:
- Formulario para los controles
- Barra de estado para mostrar la información final adicional
- Pestañas para distribuir los controles por grupos:
- Frames
- Campo de introducción para controlar la cantidad de los balances mostrados de los resultados durante el desplazamiento (scroll) repetido de los resultados tras la optimización
- Retardo en milisegundos durante el scroll de los resultados
- Botón del inicio del scroll repetido de los resultados
- Gráfico para visualizar la cantidad especificada de los balances de resultados
- Gráfico para visualizar todos los resultados
- Results
- Tabla de los mejores resultados
- Balance
- Gráfico para visualizar el balance de multisímbolos del resultado seleccionado en la tabla
- Gráfico para visualizar las reducciones del resultado seleccionado en la tabla
- Indicador de la ejecución del proceso de la repetición de los frames
El código de los métodos para crear los elementos arriba mencionados se ubica en un archivo separado y se incluye en el archivo con la clase del programa MQL:
//+------------------------------------------------------------------+ //| Clase para crear la aplicación | //+------------------------------------------------------------------+ class CProgram : public CWndEvents { private: //--- Ventana CWindow m_window1; //--- Barra de estado CStatusBar m_status_bar; //--- Pestañas CTabs m_tabs1; //--- Campos de edición CTextEdit m_curves_total; CTextEdit m_sleep_ms; //--- Botones CButton m_reply_frames; //--- Gráficos CGraph m_graph1; CGraph m_graph2; CGraph m_graph3; CGraph m_graph4; //--- Tablas CTable m_table_param; //--- Barra de progreso CProgressBar m_progress_bar; //--- public: //--- Crea la interfaz gráfica bool CreateGUI(void); //--- private: //--- Formulario bool CreateWindow(const string text); //--- Barra de estado bool CreateStatusBar(const int x_gap,const int y_gap); //--- Pestañas bool CreateTabs1(const int x_gap,const int y_gap); //--- Campos de edición bool CreateCurvesTotal(const int x_gap,const int y_gap,const string text); bool CreateSleep(const int x_gap,const int y_gap,const string text); //--- Botones bool CreateReplyFrames(const int x_gap,const int y_gap,const string text); //--- Gráficos bool CreateGraph1(const int x_gap,const int y_gap); bool CreateGraph2(const int x_gap,const int y_gap); bool CreateGraph3(const int x_gap,const int y_gap); bool CreateGraph4(const int x_gap,const int y_gap); //--- Botones bool CreateUpdateGraph(const int x_gap,const int y_gap,const string text); //--- Tablas bool CreateMainTable(const int x_gap,const int y_gap); //--- Barra de progreso bool CreateProgressBar(const int x_gap,const int y_gap,const string text); }; //+------------------------------------------------------------------+ //| Métodos para crear los controles | //+------------------------------------------------------------------+ #include "CreateGUI.mqh" //+------------------------------------------------------------------+
Como ya he dicho antes, la tabla contendrá los 100 mejores resultados de la optimización (según el beneficio total más grande). Puesto que la interfaz gráfica se crea antes del inicio de la optimización, al principio la tabla estará vacía. El número de las columnas y el texto para los encabezados va a determinarse en la clase del procesamiento de los frames de la optimización.
Creamos la tabla con el siguiente conjunto de las funciones.
- Visualización de los encabezados
- Posibilidad de la ordenación
- Selección de la fila
- Fijación de la fila seleccionada (sin poder quitar la selección)
- Cambio del ancho de la columna manualmente
- Formato en el estilo «cebra»
El código de la creación de la tabla se muestra a continuación. Para asignar la tabla a la segunda pestaña, hay que pasar el objeto de la tabla en el objeto de las pestañas, indicando el índice de la pestaña. En este caso, la clase principal para la tabla será el control «Pestañas». De esta manera, al cambiar los tamaños del área de las pestañas, el tamaño de la tabla va a cambiarse respecto a su control principal, con la condición de que esoestará establecido en las condiciones del control «Tabla».
//+------------------------------------------------------------------+ //| Crea la tabla principal | //+------------------------------------------------------------------+ bool CProgram::CreateMainTable(const int x_gap,const int y_gap) { //--- Guardamos el puntero al control principal m_table_param.MainPointer(m_tabs1); //--- Adjuntar a la pestaña m_tabs1.AddToElementsArray(1,m_table_param); //--- Propiedades m_table_param.TableSize(1,1); m_table_param.ShowHeaders(true); m_table_param.IsSortMode(true); m_table_param.SelectableRow(true); m_table_param.IsWithoutDeselect(true); m_table_param.ColumnResizeMode(true); m_table_param.IsZebraFormatRows(clrWhiteSmoke); m_table_param.AutoXResizeMode(true); m_table_param.AutoYResizeMode(true); m_table_param.AutoXResizeRightOffset(2); m_table_param.AutoYResizeBottomOffset(2); //--- Creamos el control if(!m_table_param.CreateTable(x_gap,y_gap)) return(false); //--- Añadimos el objeto al array común de los grupos de objetos CWndContainer::AddToElementsArray(0,m_table_param); return(true); }
Guardando los resultados de optimización
Para trabajar con los resultados de la optimización, ha sido implementada la clase CFrameGenerator. Vamos a usar la versión presentada en el artículo Visualizando la optimización de una estrategia comercial en MetaTrader 5. Vamos a modificarla y añadir los métodos necesarios. Tendremos que guardar no sólo el balance general y la estadística final en los frames, sino también el balance y la reducción del depósito para cada símbolo. Para guardar los balances, vamos a usar una estructura de los arrays separada, CSymbolBalance. Ella se usa para dos cosas. Va a almacenar los datos en sus arrays, que luego se pasarán al frame en el array general. Después de la optimización, los datos van a extraerse desde el array del frame y devolverse a los arrays de esta estructura para ser visualizados en los gráficos del balance de multisímbolo.
//--- Arrays para los balances de todos los símbolos struct CSymbolBalance { double m_data[]; }; //+------------------------------------------------------------------+ //| Clase para trabajar con los resultados de la optimización | //+------------------------------------------------------------------+ class CFrameGenerator { private: //--- Estructura de los balances CSymbolBalance m_symbols_balance[]; };
Como parámetro string, el frame va a recibir la enumeración de los símbolos separados con ','. Desde el principio, se suponía guardar los datos en el frame, como un informe completo en el array string. Pero en este momento, no se puede pasar los arrays string en el frame. Al intentar pasar un array tipo string a la función FrameAdd(), durante la compilación obtendremos un mensaje de error: los arrays string y estructuras que contienen los objetos no se admiten.
string arrays and structures containing objects are not allowed
Otra opción es escribir el informe en el archivo, y después pasarlo al frame. Pero esta opción tampoco nos conviene: en este caso, tendríamos que escribir los resultados en el disco duro con demasiada frecuencia.
Por eso, he decidido recopilar todos los datos necesarios en un array, y luego extraerlos usando las claves que se guardan en los parámetros del frame. Al principio de este array, van a encontrarse los índices estadísticos. Luego, siguen los datos del balance general, y después de ellos, el balance para cada símbolo por separado. Al final, van a encontrarse los datos de las reducciones para dos ejes por separado.
En el esquema de abajo, se muestran las secuencias de los datos en el array. Para abreviar, se muestra la variante de dos símbolos.
Fig. 1. Secuencia de la distribución de datos en el array.
Para determinar los índices de cada rango dentro de este array, como ha sido dicho antes, necesitaremos las claves. La cantidad de las indicaciones estadísticas es constante y se determina de antemano. En este caso, en la tabla vamos a visualizar cinco indicaciones y el número del repaso con el fin de garantizar la posibilidad del acceso a los datos de este resultado después de la optimización:
//--- Número de indicaciones estadísticas #define STAT_TOTAL 6
- Número del repaso
- Resultado de la simulación
- Beneficio (STAT_PROFIT)
- Número de transacciones (STAT_TRADES)
- Reducción (STAT_EQUITY_DDREL_PERCENT)
- Factor de recuperación (STAT_RECOVERY_FACTOR)
La cantidad de los datos del balance general y para cada símbolo será igual. Vamos a enviar este valor a la función FrameAdd(), como parámetro double. Para determinar qué símbolos han participado en la simulación, vamos a determinarlos en el historial de las transacciones durante cada repaso en la función OnTester(). Esta información será enviada a la función FrameAdd() como parámetro string.
::FrameAdd(m_report_symbols,1,data_count,stat_data);
La secuencia de los símbolos especificados en el parámetro string coincide con la secuencia de los datos en el array. De esta manera, teniendo todos estos parámetros, se puede extraer todos los datos enpaquetados en el array, sin confundir nada.
En el código de abajo se muestra el método CFrameGenerator::GetHistorySymbols(), que sirve para determinar los símbolos en el historial de las transacciones:
#include <Trade\DealInfo.mqh> //+------------------------------------------------------------------+ //| Clase para trabajar con los resultados de la optimización | //+------------------------------------------------------------------+ class CFrameGenerator { private: //--- Trabajo con transacciones CDealInfo m_deal_info; //--- Símbolos del informe string m_report_symbols; //--- private: //--- Obtenemos los símbolos del historial de la cuenta y devolvemos su cantidad int GetHistorySymbols(void); }; //+-------------------------------------------------------------------------------------+ //| Obtenemos los símbolos del historial de la cuenta y devolvemos su cantidad | //+-------------------------------------------------------------------------------------+ int CFrameGenerator::GetHistorySymbols(void) { //--- Recorremos cíclicamente por primera vez y obtenemos los símbolos del trading int deals_total=::HistoryDealsTotal(); for(int i=0; i<deals_total; i++) { //--- Obtenemos el ticket de la transacción if(!m_deal_info.SelectByIndex(i)) continue; //--- Si hay nombre del símbolo if(m_deal_info.Symbol()=="") continue; //--- Si todavía no hay esta cadena, la añadimos if(::StringFind(m_report_symbols,m_deal_info.Symbol(),0)==-1) ::StringAdd(m_report_symbols,(m_report_symbols=="")? m_deal_info.Symbol() : ","+m_deal_info.Symbol()); } //--- Obtenemos los elementos de la cadena por el separador ushort u_sep=::StringGetCharacter(",",0); int symbols_total=::StringSplit(m_report_symbols,u_sep,m_symbols_name); //--- Devolvemos el número de los símbolos return(symbols_total); }
Si resulta que en el historial de las transacciones hay más de un símbolo, el tamaño del array se establece a un elemento menos. El primer elemento se reserva para el balance general.
//--- Establecemos el tamaño del array de balances según el número de los símbolos + 1 del balance total ::ArrayResize(m_symbols_balance,(m_symbols_total>1)? m_symbols_total+1 : 1);
Después de que los datos desde el historial de las transacciones hayan sido guardados en los arrays separados, hay que colocarlos en un array general. Para eso, se utiliza el método CFrameGenerator::CopyDataToMainArray(). Aquí, aumentamos cíclicamente el array general por la cantidad de datos agregados, y copiamos los datos de las reducciones en la última iteración.
class CFrameGenerator { private: //--- Balance del resultado double m_balances[]; //--- private: //--- Copia los datos del balance al array general void CopyDataToMainArray(void); }; //+------------------------------------------------------------------+ //| Copia los datos del balance al array general | //+------------------------------------------------------------------+ void CFrameGenerator::CopyDataToMainArray(void) { //--- Número de curvas de balances int balances_total=::ArraySize(m_symbols_balance); //--- Tamaño del array del balance int data_total=::ArraySize(m_symbols_balance[0].m_data); //--- Llenamos el array general con datos for(int i=0; i<=balances_total; i++) { //--- Tamaño actual del balance int array_size=::ArraySize(m_balances); //--- Copiamos los balances al array if(i<balances_total) { //--- Copiamos el balance al array ::ArrayResize(m_balances,array_size+data_total); ::ArrayCopy(m_balances,m_symbols_balance[i].m_data,array_size); } //--- Copiamos las reducciones al array else { data_total=::ArraySize(m_dd_x); ::ArrayResize(m_balances,array_size+(data_total*2)); ::ArrayCopy(m_balances,m_dd_x,array_size); ::ArrayCopy(m_balances,m_dd_y,array_size+data_total); } } }
Las indicaciones estadísticas se añaden al principio del array general a través del método CFrameGenerator::GetStatData(). Este método recibe por referencia el array que al final será guardado en el frame. Se le establece el tamaño del array de datos de los balances más el número de indicaciones estadísticas. Los datos de los balances se colocan desde el último índice en el rango de las indicaciones estadísticas.
class CFrameGenerator { private: //--- Recibe los datos estadísticos void GetStatData(double &dst_array[],double on_tester_value); }; //+------------------------------------------------------------------+ //| Recibe los datos estadísticos | //+------------------------------------------------------------------+ void CFrameGenerator::GetStatData(double &dst_array[],double on_tester_value) { //--- Copiar el array ::ArrayResize(dst_array,::ArraySize(m_balances)+STAT_TOTAL); ::ArrayCopy(dst_array,m_balances,STAT_TOTAL,0); //--- Llenamos los primeros valores del array (STAT_TOTAL) con los resultados de la simulación dst_array[0] =0; // número del repaso dst_array[1] =on_tester_value; // valor del criterio personalizado de la optimización dst_array[2] =::TesterStatistics(STAT_PROFIT); // beneficio neto dst_array[3] =::TesterStatistics(STAT_TRADES); // número de transacciones dst_array[4] =::TesterStatistics(STAT_EQUITY_DDREL_PERCENT); // reducción máxima de equidad en por cientos dst_array[5] =::TesterStatistics(STAT_RECOVERY_FACTOR); // factor de recuperación }
Al final, las acciones arriba descritas se realizan en el método CFrameGenerator::OnTesterEvent(), que se invoca en el archivo principal del programa en la función OnTester().
//+---------------------------------------------------------------------------+ //| Prepara el array de valores del balance y lo envía dentro del frame | //| La función debe invocarse en el EA en el manejador OnTester() | //+---------------------------------------------------------------------------+ void CFrameGenerator::OnTesterEvent(const double on_tester_value) { //--- Obtenemos los datos del balance int data_count=GetBalanceData(); //--- Array para enviar los datos en el frame double stat_data[]; GetStatData(stat_data,on_tester_value); //--- Creamos el frame con los datos y lo enviamos al terminal if(!::FrameAdd(m_report_symbols,1,data_count,stat_data)) ::Print(__FUNCTION__," > Frame add error: ",::GetLastError()); else ::Print(__FUNCTION__," > Frame added, OK"); }
Los arrays de la tabla van a rellenarse el final de la optimización en el método FinalRecalculateFrames(), que se invoca en el método CFrameGenerator::OnTesterDeinitEvent(). Aquí, se realiza el recálculo final de los resultados de la optimización, se determina la cantidad de los parámetros optimizados, se rellena el array de los encabezados de la tabla, se recopilan los datos en el array de la tabla. Después de eso, los datos se ordenan según el criterio especificado.
Vamos a considerar unos métodos auxiliares, los cuales van a invocarse en el ciclo final del procesamiento de los frames. Empezamos con el método CFrameGenerator::GetParametersTotal(), en el que se determina el número de parámetros del EA que participan en la optimización.
Para obtener los parámetros del EA desde el frame, llamamos a la función FrameInputs(). Cuando pasamos el número del repaso a esta función, obtenemos el array de los parámetros y su cantidad. Al principio de su lista figuran los que han participado en la optimización, y luego van los demás. Puesto que en la tabla se muestran sólo los parámetros optimizados, hay que determinar el índice del primer parámetro no optimizado, con el fin de descartar el grupo que no tiene que entrar en la tabla. En nuestro caso, se puede indicar de antemano el primer parámetro externo no optimizado del EA al que va a orientarse el programa. En este caso, es Symbols. Una vez determinado el índice, se puede calcular la cantidad de los parámetros optimizados del EA.
class CFrameGenerator { private: //--- Primer parámetro no optimizado string m_first_not_opt_param; //--- private: //--- Obtiene la cantidad de parámetros optimizados void GetParametersTotal(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CFrameGenerator::CFrameGenerator(void) : m_first_not_opt_param("Symbols") { } //+------------------------------------------------------------------+ //| Obtiene la cantidad de parámetros optimizados | //+------------------------------------------------------------------+ void CFrameGenerator::GetParametersTotal(void) { //--- En el primer frame, determinamos la cantidad de parámetros optimizados if(m_frames_counter<1) { //--- Obtenemos los parámetros de entrada del EA para los que ha sido formado el frame ::FrameInputs(m_pass,m_param_data,m_par_count); //--- Encontramos el índice del primer parámetro no optimizado int limit_index=0; int params_total=::ArraySize(m_param_data); for(int i=0; i<params_total; i++) { if(::StringFind(m_param_data[i],m_first_not_opt_param)>-1) { limit_index=i; break; } } //--- Cantidad de parámetros optimizados m_param_total=(m_par_count-(m_par_count-limit_index)); } }
Los datos de la table van a almacenarse en la estructura de los arrays CReportTable. Después de averiguar el número de los parámetros optimizados del EA, aparece la posibilidad de determinar y establecer el número de las columnas de la tabla. Eso se hace en el método CFrameGenerator::SetColumnsTotal(). Originalmente, el número de las filas es igual a cero.
//--- Arrays de la tabla struct CReportTable { string m_rows[]; }; //+------------------------------------------------------------------+ //| Clase para el trabajo con los resultados de la optimización | //+------------------------------------------------------------------+ class CFrameGenerator { private: //--- Tabla para el informe CReportTable m_columns[]; //--- private: //--- Establecer el número de las columnas de la tabla void SetColumnsTotal(void); }; //+------------------------------------------------------------------+ //| Establecer el número de las columnas de la tabla | //+------------------------------------------------------------------+ void CFrameGenerator::SetColumnsTotal(void) { //--- Determinamos el número de las columnas para la tabla de resultados if(m_frames_counter<1) { int columns_total=int(STAT_TOTAL+m_param_total); ::ArrayResize(m_columns,columns_total); for(int i=0; i<columns_total; i++) ::ArrayFree(m_columns[i].m_rows); } }
Las filas se añaden en el método CFrameGenerator::AddRow(). Durante el proceso del repaso de todos los frames, en la tabla entrarán sólo los resultados que contienen las transacciones. En las primeras columnas de la tabla, empezando desde el número del repaso, van a ubicarse las indicaciones estadísticas, y luego, los parámetros optimizados del EA. Cuando se obtienen los parámetros desde el frame, se muestran en el siguiente formato "parameterN=valueN" [nombre del parámetro][separador][valor del parámetro]. Necesitamos sólo los valores de los parámetros que tienen que entrar en la tabla. Por eso, dividimos la cadena por el separador ‘=’ y guardamos el valor desde el segundo elemento del array.
class CFrameGenerator { private: //--- Añade la fila de los datos void AddRow(void); }; //+------------------------------------------------------------------+ //| Añade la fila de los datos | //+------------------------------------------------------------------+ void CFrameGenerator::AddRow(void) { //--- Establecemos el número de las columnas en la tabla SetColumnsTotal(); //--- Salir si no hay transacciones if(m_data[3]<1) return; //--- Llenamos la tabla int columns_total=::ArraySize(m_columns); for(int i=0; i<columns_total; i++) { //--- Añadimos la fila int prev_rows_total=::ArraySize(m_columns[i].m_rows); ::ArrayResize(m_columns[i].m_rows,prev_rows_total+1,RESERVE); //--- Número del repaso if(i==0) { m_columns[i].m_rows[prev_rows_total]=string(m_pass); continue; } //--- Indicaciones estadísticas if(i<STAT_TOTAL) m_columns[i].m_rows[prev_rows_total]=string(m_data[i]); //--- Parámetros optimizados del EA else { string array[]; if(::StringSplit(m_param_data[i-STAT_TOTAL],'=',array)==2) m_columns[i].m_rows[prev_rows_total]=array[1]; } } }
Recogemos los encabezados para la tabla en el método CFrameGenerator::GetHeaders(), pero sólo el primer elemento desde el array de los elementos de la cadena dividida:
class CFrameGenerator { private: //--- Obtiene los encabezados para la tabla void GetHeaders(void); }; //+------------------------------------------------------------------+ //| Obtiene los encabezados para la tabla | //+------------------------------------------------------------------+ void CFrameGenerator::GetHeaders(void) { int columns_total =::ArraySize(m_columns); //--- Encabezados ::ArrayResize(m_headers,STAT_TOTAL+m_param_total); for(int c=STAT_TOTAL; c<columns_total; c++) { string array[]; if(::StringSplit(m_param_data[c-STAT_TOTAL],'=',array)==2) m_headers[c]=array[0]; } }
Para indicar al programa según qué criterio hay que elegir 100 resultados para la tabla, usamos un método simple CFrameGenerator::ColumnSortIndex(). Le pasamos el índice de la columna. Tras la finalización de la optimización, la tabla de los resultados será ordenada en orden descendiente precisamente por este índice, mientras que los 100 resultados superiores entrarán en la tabla para visualizarse en la interfaz gráfica. Por defecto, se establece la tercera columna (índice 2), es decir, la ordenación será por el beneficio máximo.
class CFrameGenerator { private: //--- Indice de la columna ordenada uint m_column_sort_index; //--- public: //--- Establecer el índice de la columna por la que se realiza la ordenación de la tabla void ColumnSortIndex(const uint index) { m_column_sort_index=index; } }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CFrameGenerator::CFrameGenerator(void) : m_column_sort_index(2) { }
Si queremos seleccionar los resultados según otro criterio, el método CFrameGenerator::ColumnSortIndex() se invoca en el método CProgram::OnTesterInitEvent() al principio de la optimización:
//+------------------------------------------------------------------+ //| Evento del inicio del proceso de la optimización | //+------------------------------------------------------------------+ void CProgram::OnTesterInitEvent(void) { ... m_frame_gen.ColumnSortIndex(3); ... }
Como resultado, el método CFrameGenerator::FinalRecalculateFrames() para el recálculo final ahora trabaja según el siguiente algoritmo.
- Pasamos el puntero de los frames al principio de la lista. El reseteo del contador del frame y anulación de los arrays.
- Luego, recorremos cíclicamente todos los frames y:
- obtenemos la cantidad de parámetros optimizados,
- distribuimos los resultados negativos y positivos por los arrays,
- añadimos la fila de datos a la tabla.
- Después del ciclo del recorrido por los frames, obtenemos los encabezados de la tabla.
- Luego, ordenamos la tabla por la columna especificada en los ajustes.
- Terminamos el método de la actualización del gráfico con los resultados de la optimización.
Código del método CFrameGenerator::FinalRecalculateFrames():
class CFrameGenerator { private: //--- Recálculo final de datos de todos los frames tras la optimización void FinalRecalculateFrames(void); }; //+------------------------------------------------------------------------+ //| Recálculo final de datos de todos los frames tras la optimización | //+------------------------------------------------------------------------+ void CFrameGenerator::FinalRecalculateFrames(void) { //--- Pasamos el puntero de los frames al principio ::FrameFirst(); //--- Reseteo del contador y los arrays ArraysFree(); m_frames_counter=0; //--- Iniciamos el recorrido de los frames while(::FrameNext(m_pass,m_name,m_id,m_value,m_data)) { //--- Obtiene la cantidad de parámetros optimizados GetParametersTotal(); //--- Resultado negativo if(m_data[m_profit_index]<0) AddLoss(m_data[m_profit_index]); //--- Resultado positivo else AddProfit(m_data[m_profit_index]); //--- Añade la fila de los datos AddRow(); //--- Aumentamos el contador de frames procesados m_frames_counter++; } //--- Obtenemos los encabezados para la tabla GetHeaders(); //--- Número de columnas y filas int rows_total =::ArraySize(m_columns[0].m_rows); //--- Ordenamos la tabla por la columna especificada QuickSort(0,rows_total-1,m_column_sort_index); //--- Actualizamos las series en el gráfico CCurve *curve=m_graph_results.CurveGetByIndex(0); curve.Name("P: "+(string)ProfitsTotal()); curve.Update(m_profit_x,m_profit_y); //--- curve=m_graph_results.CurveGetByIndex(1); curve.Name("L: "+(string)LossesTotal()); curve.Update(m_loss_x,m_loss_y); //--- Propiedades del eje horizontal CAxis *x_axis=m_graph_results.XAxis(); x_axis.Min(0); x_axis.Max(m_frames_counter); x_axis.DefaultStep((int)(m_frames_counter/8.0)); //--- Actualizar el gráfico m_graph_results.CalculateMaxMinValues(); m_graph_results.CurvePlotAll(); m_graph_results.Update(); }
A continuación, examinaremos los métodos que permiten obtener los datos desde los frames por la solicitud del usuario.
Extracción de datos del frame
Antes, hemos analizado la estructura del array general con la secuencia de datos de categorías diferentes. Ahora, hay que comprender de qué manera van a extraerse los datos desde este array. Más arriba hemos dicho que el frame contiene la secuencia de símbolos y el tamaño de los arrays del balance en forma de las claves. Si el tamaño de los arrays de balances fuera igual al tamaño de los arrays de reducciones, la determinación de los índices de todos los rangos de datos enpaquetados se realizaría según la misma fórmula en el ciclo (tal como se muestra en la esquema de abajo). Pero los tamaños de arrays son diferentes. Por eso, en la última iteración en el ciclo, hay que determinar cuántos elementos quedan en el rango de datos que corresponde a las reducciones del depósito, y dividirlo en dos, por que los tamaños de los arrays de reducciones son iguales.
Fig. 2. Esquema de parámetros para el cálculo del índice del array de la siguiente categoría.
Para obtener los datos desde el frame, ha sido implementado el método CFrameGenerator::GetFrameData(). Vamos a analizarlo más detalladamente.
Al principio del método, hay que pasar el puntero de los frames al principio de la lista. Luego, iniciamos el proceso del recorrido de todos los frames con los resultados de la optimización. Hay que encontrar el frame cuyo número del recorrido ha sido pasado al método como argumento. Si ha sido encontrado, el programa sigue este algoritmo.
- Obtenemos el tamaño del array general con datos del frame.
- Obtenemos los elementos de la cadena del parámetro string y su cantidad. Si resulta que hay más de un símbolo, el número de los balances en el array será más a uno. Es decir, el primer rango es el balance general, los demás corresponden a los balances de los símbolos.
- Luego, hay que pasar los datos en los arrays de los balances. Iniciamos el ciclo para extraer los datos desde el array general (el número de iteraciones es igual al número de balances). Para determinar el índice a partir del cual es necesario copiar los datos, será suficiente hacer el desplazamiento según el número de las indicaciones estadísticas (STAT_TOTAL) y multiplicar el índice de la iteración ( i ) por el tamaño del array del balance (m_value). Así, en cada iteración, obtenemos los datos de todos los balances en los arrays separados.
- En la última iteración, obtenemos los datos de las reducciones en los arrays separados. Son los últimos datos en el array, por eso simplemente hay que averiguar la cantidad restante de los elementos y dividirla por 2. Luego, obtenemos consecutivamente los datos de las reducciones en dos pasos.
- En el último paso, actualizamos los gráficos con datos nuevos, y detenemos el ciclo del recorrido de frames.
class CFrameGenerator { public: //--- Obtiene los datos según el número del frame especificado void GetFrameData(const ulong pass_number); }; //+------------------------------------------------------------------+ //| Obtiene los datos según el número del frame especificado | //+------------------------------------------------------------------+ void CFrameGenerator::GetFrameData(const ulong pass_number) { //--- Pasamos el puntero de los frames al principio ::FrameFirst(); //--- Extracción de datos while(::FrameNext(m_pass,m_name,m_id,m_value,m_data)) { //--- Los números de recorridos no coinciden, pasamos al siguiente if(m_pass!=pass_number) continue; //--- Tamaño del array con datos int data_total=::ArraySize(m_data); //--- Obtenemos los elementos de la cadena por el separador ushort u_sep =::StringGetCharacter(",",0); int symbols_total =::StringSplit(m_name,u_sep,m_symbols_name); int balances_total =(symbols_total>1)? symbols_total+1 : symbols_total; //--- Establecemos el tamaño para el array del número de balances ::ArrayResize(m_symbols_balance,balances_total); //--- Distribuimos los datos en los arrays for(int i=0; i<balances_total; i++) { //--- Liberar el array de datos ::ArrayFree(m_symbols_balance[i].m_data); //--- Determinamos el índice a partir del cual hay que copiar los datos int src_index=STAT_TOTAL+int(i*m_value); //--- Copiamos los datos al array de la estructura de balances ::ArrayCopy(m_symbols_balance[i].m_data,m_data,0,src_index,(int)m_value); //--- Si se trata de la última iteración, obtenemos los datos de las reducciones if(i+1==balances_total) { //--- Obtenemos la cantidad de datos restantes y el tamaño para los arrays según dos ejes double dd_total =data_total-(src_index+(int)m_value); double array_size =dd_total/2.0; //--- Indice desde el cual empezamos a copiar src_index=int(data_total-dd_total); //--- Establecemos el tamaño para los arrays de reducciones ::ArrayResize(m_dd_x,(int)array_size); ::ArrayResize(m_dd_y,(int)array_size); //--- Copiamos consecutivamente los datos ::ArrayCopy(m_dd_x,m_data,0,src_index,(int)array_size); ::ArrayCopy(m_dd_y,m_data,0,src_index+(int)array_size,(int)array_size); } } //--- Actualizar los gráficos y detener el ciclo UpdateMSBalanceGraph(); UpdateDrawdownGraph(); break; } }
Para obtener los datos desde las celdas del array de la tabla, llamamos al método público CFrameGenerator::GetValue(), indicando el índice de la columna y de la fila de la tabla en los argumentos.
class CFrameGenerator { public: //--- Devuelve los valores de la celda especificada string GetValue(const uint column_index,const uint row_index); }; //+------------------------------------------------------------------+ //| Devuelve los valores de la celda especificada | //+------------------------------------------------------------------+ string CFrameGenerator::GetValue(const uint column_index,const uint row_index) { //--- Comprobar la superación del rango de las columnas uint csize=::ArraySize(m_columns); if(csize<1 || column_index>=csize) return(""); //--- Comprobar la superación del rango de las filas uint rsize=::ArraySize(m_columns[column_index].m_rows); if(rsize<1 || row_index>=rsize) return(""); //--- return(m_columns[column_index].m_rows[row_index]); }
Visualización de datos e interacción con la interfaz gráfica
Para actualizar los gráficos con los datos de los balances y reducciones, en la clase CFrameGenerator han sido declarados dos objetos más del tipo CGraphic. Igual como en el caso de otros objetos de este tipo, en la clase CFrameGenerator, es necesario completarlos con los punteros a los elementos de la interfaz gráfica al principio de la optimización en el método CFrameGenerator::OnTesterInitEvent().
#include <Graphics\Graphic.mqh> //+------------------------------------------------------------------+ //| Clase para trabajar con los resultados de la optimización | //+------------------------------------------------------------------+ class CFrameGenerator { private: //--- Punteros a los gráficos para visualizar los datos CGraphic *m_graph_ms_balance; CGraphic *m_graph_drawdown; //--- public: //--- Manejadores de los eventos del Probador de Estrategias void OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_results,CGraphic *graph_ms_balance,CGraphic *graph_drawdown); }; //+------------------------------------------------------------------+ //| Debe invocarse en el manejador OnTesterInit() | //+------------------------------------------------------------------+ void CFrameGenerator::OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_results, CGraphic *graph_ms_balance,CGraphic *graph_drawdown) { m_graph_balance =graph_balance; m_graph_results =graph_results; m_graph_ms_balance =graph_ms_balance; m_graph_drawdown =graph_drawdown; }
Los datos de la tabla de la interfaz gráfica se visualizan a través del método CProgram::GetFrameDataToTable(). Definimos la cantidad de columnas obteniendo los encabezados de la tabla desde el objeto CFrameGenerator en el array. Después de eso, establecemos el tamaño de la tabla (100 filas) en la interfaz gráfica. Luego, establecemos los encabezados y el tipo de datos.
Ahora, es necesario inicializar la tabla con los resultados de la optimización. Establecemos los valores usando el método CTable::SetValue(). Para obtener los valores desde las celdas de la tabla de datos, se usa el método CFrameGenerator::GetValue(). Para que los cambios realizados se muestren, es necesario actualizar la tabla.
class CProgram { private: //--- Obtiene los datos de los frames en la tabla de los resultados de la optimización void GetFrameDataToTable(void); }; //+------------------------------------------------------------------------+ //| Obtenemos los datos en la tabla de los resultados de la optimización | //+------------------------------------------------------------------------+ void CProgram::GetFrameDataToTable(void) { //--- Obtenemos los encabezados string headers[]; m_frame_gen.CopyHeaders(headers); //--- Establecemos el tamaño de la tabla uint columns_total=::ArraySize(headers); m_table_param.Rebuilding(columns_total,100,true); //--- Establecemos los encabezados y el tipo de datos for(uint c=0; c<columns_total; c++) { m_table_param.DataType(c,TYPE_DOUBLE); m_table_param.SetHeaderText(c,headers[c]); } //--- Llenamos la tabla con datos desde los frames for(uint c=0; c<columns_total; c++) { for(uint r=0; r<m_table_param.RowsTotal(); r++) { if(c==1 || c==2 || c==4 || c==5) m_table_param.SetValue(c,r,m_frame_gen.GetValue(c,r),2); else m_table_param.SetValue(c,r,m_frame_gen.GetValue(c,r),0); } } //--- Actualizar la tabla m_table_param.Update(true); m_table_param.GetScrollHPointer().Update(true); m_table_param.GetScrollVPointer().Update(true); }
El método CProgram::GetFrameDataToTable() se invoca cuando se finaliza el proceso de la optimización de los parámetros del Asesor Experto en el método OnTesterDeinit(). Después de eso, la interfaz gráfica se hace disponible para el usuario. Si va a la pestaña Results, puede observar los resultados de la optimización seleccionados según el criterio especificado. En nuestro ejemplo, la selección se realiza según la indicación en la segunda columna (Profit).
Fig. 3. Tabla de resultados de la optimización en la interfaz gráfica.
Ahora, veremos de qué manera el usuario puede ver los balances de multisímbolos de los resultados en esta tabla. Si seleccionamos alguna fila de la tabla, se genera el evento de usuario ON_CLICK_LIST_ITEM con el identificador de la tabla. Según él, podemos determinar de qué tabla ha sido recibido este mensaje (si hay más de una). Puesto que en la primera columna de la tabla se guarda el número del repaso, se puede obtener los datos de este resultado, pasando este número al método CFrameGenerator::GetFrameData().
//+------------------------------------------------------------------+ //| Manejador de eventos | //+------------------------------------------------------------------+ void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- Evento del clic en las filas de la tabla if(id==CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM) { if(lparam==m_table_param.Id()) { //--- Obtenemos el número del repaso desde la tabla ulong pass=(ulong)m_table_param.GetValue(0,m_table_param.SelectedItem()); //--- Obtenemos los datos según el número del repaso m_frame_gen.GetFrameData(pass); } //--- return; } ... }
Cada vez cuando el usuario seleccione una fila en la tabla, el gráfico de los balances de multisímbolos se actualiza en la pestaña Balance:
Fig. 4. Demostración del resultado obtenido.
Hemos obtenido una herramienta bastante cómoda para revisar rápidamente los resultados de multisímbolos de las pruebas.
Conclusión
He mostrado una opción más de las posibles de qué manera se puede trabajar con los resultados de la optimización tras su finalización. Este tema todavía no está completada, y es necesario y se debe seguir desarrolándolo. Usando la librería para el diseño de las interfaces gráficas, se puede crear muchas soluciones interesantes y convenientes. Propongan sus ideas en los comentarios para el artículo: probablemente, en algún artículo siguiente, aparecerá una herramienta necesaria precisamente para su trabajo con los resultados de la optimización.
Más abajo Usted puede descargar los archivos adjuntos para el testeo y el análisis detallado del código presentado en el artículo.
Nombre del archivo | Comentario |
---|---|
MacdSampleMSFrames.mq5 | EA modificado desde la entrega estándar MACD Sample |
Program.mqh | Archivo con la clase del programa |
CreateGUI.mqh | Archivo con la implementación de los métodos desde la clase del programa en el archivo Program.mqh |
Strategy.mqh | Archivo con la clase modificada de la estrategia MACD Sample (versión de multisímbolos) |
FormatString.mqh | Archivo con las funciones auxiliares para el formateo de las cadenas |
FrameGenerator.mqh | Clase para el trabajo con los resultados de la optimización |
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/4562
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso