Visualización de estrategias en MQL5: distribuimos los resultados de la optimización en gráficos de criterios
Contenido
- Introducción
- Funcionamiento
- Clases adaptadas a nuestras necesidades
- El control Tab Control
- La clase de tablas
- La clase Progress-Bar
- La clase de gráficos estadísticos
- La clase de visor de frames
- Conexión de funciones al asesor experto
- Conclusión
Introducción
El recurso mql5.com contiene tanta información que cada vez, hojeando una y otra vez catálogos de artículos o información de referencia, o tutoriales, seguro que encuentras algo nuevo e interesante.
Esta vez me he topado con un artículo sencillo y, a primera vista, sin complicaciones, que describe brevemente el simulador de estrategias. Parece sencillo y se sabe desde hace mucho tiempo, pero.... Pero la última parte del artículo me resultó intrigante. Simplemente se propone conectar un pequeño código al asesor experto, añadirle unos cuantos manejadores estándar, y..., y el optimizador habitual del simulador de estrategias de la plataforma MetaTrader 5 se convierte en visual. Hmmm... Interesante.
Así que empecé a investigar. Finalmente, me surgió la idea de mejorar ligeramente el aspecto de los resultados de la optimización.
Haremos lo siguiente: el asesor experto abrirá una nueva ventana con cinco pestañas. La primera tendrá un gráfico de todas las pasadas, donde cada nueva pasada se mostrará mediante una línea de balance. Las otras cuatro pestañas también tendrán gráficos, pero estarán disponibles cuando finalice la optimización. Cada una de estas pestañas mostrará datos sobre las tres primeras pasadas para uno de los cuatro criterios de optimización. Y en cada pestaña habrá dos tablas, una con los resultados de la pasada de optimización y otra los ajustes del asesor experto para esta pasada:
- La pestaña Optimization:
- tabla de resultados de la optimización de la siguiente pasada,
- tabla de parámetros de entrada del asesor experto para esta pasada,
- gráfico de balance de la pasada de optimización actualmente completada,
- botón Replay para reproducir la optimización realizada.
- La pestaña Sharpe Ratio:
- tabla de resultados de la optimización de la pasada seleccionada (una de las tres mejores según el ratio de Sharpe),
- tabla de parámetros de entrada del asesor experto para la pasada seleccionada (una de las tres mejores según el Ratio de Sharpe),
- gráficos de balance de las tres mejores ejecuciones de optimización del Ratio de Sharpe.
- botón-conmutador (de tres posiciones) para seleccionar uno de los tres mejores resultados de optimización del Ratio de Sharpe.
- La pestaña Net Profit:
- tabla de resultados de la optimización de la pasada seleccionada (una de las tres mejores en términos de Beneficio Total),
- tabla de parámetros de entrada del asesor experto para la pasada seleccionada (una de las tres mejores según el Beneficio Total),
- gráficos de balance de las tres mejores pasadas de optimización en términos de Beneficio Total,
- botón-conmutador (de tres posiciones) para seleccionar uno de los tres mejores resultados de optimización según el Beneficio Total.
- La pestaña Profit Factor:
- tabla de resultados de la optimización de la pasada seleccionada (una de las tres mejores en términos de Rentabilidad),
- tabla de parámetros de entrada del asesor experto para la pasada seleccionada (una de las tres mejores en términos de Rentabilidad),
- gráficos de balance de las tres mejores pasadas de optimización según la Rentabilidad,
- botón-conmutador (de tres posiciones) para seleccionar uno de los tres mejores resultados de optimización según la Rentabilidad.
- La pestaña Recovery Factor:
- tabla de resultados de la optimización de la pasada seleccionada (una de las tres mejores pasadas según el Factor de Recuperación),
- tabla de parámetros de entrada del asesor experto para la pasada seleccionada (una de las tres mejores según el Factor de Recuperación),
- gráficos de balance de las tres mejores ejecuciones de optimización según el Factor de Recuperación,
- botón-conmutador (de tres posiciones) para seleccionar uno de los tres mejores resultados de optimización del Factor de Recuperación.
Para implementar un conjunto de pestañas, implementaremos controles a partir de las cuales compondremos el control Tab Control. Nos saltaremos el proceso de creación de controladores en este artículo, ofreciendo solo un archivo de clase ya hecho. En futuros artículos volveremos a la descripción de dichas clases para crear algunos controles que pueden resultar útiles en el futuro.
Para mostrar información sobre los parámetros de las pasadas, necesitaremos clases de tablas, que tomaremos ya preparadas del artículo "Capacidades de SQLite en MQL5: Ejemplo de panel interactivo con estadísticas comerciales por símbolos y números mágicos", y modificaremos ligeramente las clases de tablas para una creación más cómoda de las mismas y la muestra de texto en sus celdas.
Para poner en práctica la idea, tomaremos los códigos para trabajar con frames de optimización que se adjuntan en el artículo mencionado y haremos nuestras propias clases basadas en ellos, tratando de mantener el concepto tanto como sea posible. Dado que el artículo no describe el proceso de trabajo con frames y con un asesor experto que funcione en el modo frame, hoy trataremos de entender este sistema aquí.
Funcionamiento
Vamos a recurrir al Manual de MQL5, y lo que allí se dice respecto al funcionamiento del simulador de estrategias y su optimizador:
... Una función especialmente importante del simulador será la optimización multiflujo, que puede realizarse mediante programas agentes locales y distribuidos (en red), incluido MQL5 Cloud Network. Una sola ejecución de prueba (con parámetros de entrada específicos del asesor experto), iniciada manualmente por el usuario, o una con un conjunto de ejecuciones llamadas por la optimización (cuando se realiza una búsqueda de valores de parámetros en rangos especificados) se efectuará en un programa separado, mediante un agente. Técnicamente, es el archivo metatester64.exe, y podrá ver copias de sus procesos en el Administrador de tareas de Windows durante las pruebas y la optimización. Por ello, el simulador será multiflujo.
El terminal es un despachador que distribuye tareas a agentes locales y remotos. Así, inicia los agentes locales por sí mismo si es necesario. Al realizar la optimización, se inician por defecto varios agentes, cuyo número corresponde al número de núcleos del procesador. Después de completar la siguiente tarea de prueba del asesor experto con los parámetros especificados, el agente retorna los resultados al terminal.
Cada agente crea su propio entorno comercial y programático. Todos los agentes están aislados entre sí y del terminal cliente.
Qué podemos entender de la descripción: cada instancia del asesor experto bajo prueba se inicia en su propio agente de pruebas, y cada pasada (sus datos finales) se envían desde el agente al terminal.
Existe un conjunto de manejadores para intercambiar datos entre el terminal y los agentes:
- OnTesterInit() - se llama en los asesores expertos cuando ocurre el evento TesterInit para realizar las acciones necesarias antes de comenzar la optimización en el simulador de estrategias.
- OnTester() - se llama en los asesores expertos cuando ocurre el evento Tester para realizar las acciones necesarias al final de la prueba.
- OnTesterPass() - se llamada en los asesores expertos cuando ocurre el evento TesterPass para procesar un nuevo frame de datos durante la optimización del asesor experto.
- OnTesterDeinit() - se ejecuta en los asesores expertos cuando se produce el evento TesterDeinit para realizar las acciones necesarias una vez finalizada la optimización del asesor experto.
Si el asesor experto tiene alguno de los manejadores OnTesterInit(), OnTesterDeinit() (estos dos manejadores siempre trabajan en pareja, no puede tener solo uno de ellos), OnTesterPass(), el asesor experto se iniciará en una ventana de terminal aparte en un modo frame especial:
Existen 3 eventos especiales en MQL5 para gestionar el proceso de optimización y transferir los resultados aleatorios de la aplicación (además de los indicadores comerciales) de los agentes al terminal: OnTesterInit, OnTesterDeinit, OnTesterPass. Describiendo en el código los manejadores correspondientes, el programador podrá realizar las acciones necesarias antes de iniciar la optimización, una vez concluida la misma y al final de cada una de las pasadas de optimización.
Todos los manejadores son opcionales. La optimización también funciona sin ellos. También debemos entender que los 3 eventos solo funcionan durante la optimización, no una sola prueba.
El asesor experto con estos manejadores se carga automáticamente en un gráfico terminal independiente con el símbolo y el periodo especificados en el simulador. Esta copia del asesor experto no comercia, sino que realiza acciones puramente de servicio. Todos los demás manejadores de eventos no funcionan en él, concretamente OnInit, OnDeinit, OnTick.
Durante la optimización, solo se ejecuta una instancia del asesor experto en el terminal y, si es necesario, acepta los frames entrantes. Pero debemos indicar una vez más que tal instancia del asesor experto se inicia solo si uno de los tres manejadores de eventos descritos está presente en su código.
Tras la finalización de cada pasada individual del optimizador, se generará el evento OnTester() en la instancia del asesor experto que se ejecuta en el agente. Desde el manejador de este evento podemos enviar datos sobre la pasada al asesor experto que trabaja en un gráfico separado en un modo frame especial. El paquete de datos sobre un pasada completada enviada al asesor experto en el gráfico se llama frame, y contiene información sobre el número de pasada, los valores de las variables de entrada del asesor experto con las que se ha iniciado la misma, y los resultados de esta pasada.
Todos estos datos son recibidos por el asesor experto, que genera el evento TesterPass, que es procesado en el manejador OnTesterPass(), donde podemos leer los datos de la pasada y realizar algunas acciones (en este caso, por ejemplo, trazar el gráfico de balance de esta pasada y realizar otras acciones de servicio).
Para enviar los datos sobre la pasada del agente al asesor experto en el gráfico del terminal, debemos utilizar la función FrameAdd(). El frame actual (pasada completada) se enviará desde el agente al asesor experto y se procesará allí en el manejador OnTesterPass().
Como podemos ver, algunas funciones operan en el agente en la instancia del asesor experto iniciada en él, mientras que algunas funciones operan en el asesor experto en el gráfico terminal trabajando en el modo frame. Pero todas ellas, por supuesto, deben ser descritas dentro del código del asesor experto.
Como resultado, la secuencia de la asesor experto y nuestras acciones a la hora de transferir datos entre el agente y el terminal será la siguiente:
- En el manejador OnTesterInit (una instancia del asesor experto en un gráfico en el terminal), es necesario preparar todas las construcciones gráficas, a saber, un gráfico aparte en el que se inicia el asesor experto en el modo frame, y el rellenado de este gráfico: el gráfico de balance, las tablas con parámetros y resultados, el objeto de control de pestañas y botones para seleccionar acciones en las pestañas;
- En el manejador OnTester (una instancia asesor experto en el agente), es necesario recoger toda la información sobre la pasada completada; el resultado del balance de cada operación de cierre debe escribirse en un array, luego los resultados de esta pasada deben ser obtenidos y escritos en el array, y todos estos datos deben ser enviados al asesor experto usando FrameAdd();
- En el manejador OnTesterPass (instancia de asesor experto en el gráfico de la terminal) recibimos el siguiente frame enviado desde el agente usando FrameAdd(), leemos sus datos y dibujamos un gráfico de balance en el gráfico, creamos un objeto de frame y lo guardamos en un array para su posterior clasificación y selección según los criterios de optimización;
- En los manejadores OnTesterDeinit y OnChartEvent (una instancia de asesor experto en el gráfico en el terminal) se realiza el trabajo con los datos de optimización después de su finalización, la repetición del proceso de optimización y la visualización de los mejores resultados según algunos criterios de optimización.
Clases adaptadas a nuestras necesidades
Para crear el control de pestañas, hemos creado un archivo con un conjunto de controles Controls.mqh. El archivo se adjunta al final del artículo y deberá colocarse directamente en la carpeta donde vamos a escribir el asesor experto de prueba, por ejemplo, en el directorio del terminal \MQL5\Experts\FrameViewer\Controls.mqh.
Aquí no veremos cada clase creada de cada control. Vamos a hacer un breve resumen.
Hemos implementado un total de diez clases para ocho controles independientes:
| # | Clase | Clase padre | Descripción | Propósito |
|---|---|---|---|---|
| 1 | CBaseCanvas | Objeto | Clase básica de dibujado | Lienzo básico. Contiene métodos para fijar y redimensionar y posicionar, ocultar y mostrar |
| 2 | CPanel | CBaseCanvas | Clase de panel | Contiene los métodos para establecer y cambiar los colores y controladores de los eventos del ratón. Permite adjuntar los controles hijos |
| 3 | CLabel | CPanel | Clase de etiqueta de texto | Muestra texto en el lienzo en las coordenadas establecidas |
| 4 | CButton | CLabel | Clase de botón simple | Botón normal con un estado no fijo. Reacciona a los clics del cursor y del ratón cambiando de color |
| 5 | CButtonTriggered | CButton | Clase de botón de dos posiciones | Botón con dos estados: Encendido/Apagado. Responde a los movimientos del cursor, los clics del ratón y los cambios de estado cambiando de color. |
| 6 | CTabButton | CButtonTriggered | Clase de botón para una pestaña | Botón de dos posiciones al que le falta el frame donde se conecta con el campo de pestaña |
| 7 | CButtonSwitch | CPanel | Clase de botón de conmutación | Panel con dos o más botones de dos posiciones, en el que solo uno puede tener un estado Activado. Permite añadir nuevos botones a los ya existentes |
| 8 | CTabWorkArea | Objeto | Clase de espacio de trabajo de pestañas | Objeto con dos clases básicas de dibujado como parte de él: para el fondo y el primer plano |
| 9 | CTab | CPanel | Clase de objeto de pestaña | Panel que incluye un botón y un campo. El campo de pestaña contiene el área de trabajo donde se realiza el dibujado |
| 10 | CTabControl | CPanel | Clase de objeto de gestión de pestañas | Panel que permite añadir y gestionar objetos de pestaña a su composición |
Una vez que un objeto de control ha sido creado con éxito, su método Create() deberá ser llamado para cada objeto, especificando sus coordenadas y dimensiones. Después de ello, el elemento estará listo para trabajar con él.
El elemento de control con manejadores de eventos implementados envía eventos personalizados al gráfico del programa de control, que puede utilizarse para determinar qué se ha hecho en el objeto:
| # | Clase | Evento | Identificador | lparam | dparam | sparam |
|---|---|---|---|---|---|---|
| 1 | CButton | Hacer clic en un objeto | (ushort)CHARTEVENT_CLICK | Coordenada X del cursor | Coordenada Y del cursor | Nombre del objeto de botón |
| 2 | CButtonTriggered | Hacer clic en un objeto | (ushort)CHARTEVENT_CLICK | Coordenada X del cursor | Coordenada Y del cursor | Nombre del objeto de botón |
| 3 | CTabButton | Hacer clic en un objeto | (ushort)CHARTEVENT_CLICK | Coordenada X del cursor | Coordenada Y del cursor | Nombre del objeto de botón |
| 4 | CButtonSwitch | Pulsar el botón de objeto | (ushort)CHARTEVENT_CLICK | ID de botón | 0 | Nombre del objeto de botón-conmutador |
Podemos ver en la tabla que, para simplificar el código, no hay ninguna referencia del Tab Control al gráfico del programa de eventos personalizado. Si el programa requiere una reacción al cambiar de pestañas, entonces el evento se podrá definir mediante el evento de clic en el botón de pestaña TabButton. Por el nombre del botón se podrá averiguar el número de pestaña, o bien solicitar el índice de la pestaña seleccionada al objeto TabControl, etc.
En cualquier caso, a continuación analizaremos con detalle dichas clases a la hora de crear diversos controladores útiles para utilizar en nuestros programas.
Ahora deberemos modificar ligeramente la clase de tabla presentada en el artículo y descargada (archivo Dashboard.mqh); copiar solo el código de la clase de tabla del archivo (líneas 12 - 285) y guardar el código copiado en la carpeta \MQL5\Experts\FrameViewer\ en el archivo Table.mqh.
Completaremos la clase para que trabajar con tablas y datos tabulares sea un poco más cómodo.
Conectaremos al archivo el archivo de clase de array dinámico de los punteros a las instancias de la clase CObject y sus herederos CArrayObj y el archivo de la clase para la creación simplificada de dibujos personalizados CCanvas:
//+------------------------------------------------------------------+ //| Table.mqh | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #include <Arrays\ArrayObj.mqh> #include <Canvas\Canvas.mqh>
En la sección privada de la clase de celda de tabla , añadiremos nuevas variables para almacenar la anchura, la altura y el color del texto de la celda:
//+------------------------------------------------------------------+ //| Класс ячейки таблицы | //+------------------------------------------------------------------+ class CTableCell : public CObject { private: int m_row; // Строка int m_col; // Столбец int m_x; // Координата X int m_y; // Координата Y int m_w; // Ширина int m_h; // Высота string m_text; // Текст в ячейке color m_fore_color; // Цвет текста в ячейке public:
En la sección pública, añadiremos los métodos para leer y establecer las nuevas propiedades, así como un método que envía el texto escrito en la celda al objeto canvas especificado:
public: //--- Методы установки значений void SetRow(const uint row) { this.m_row=(int)row; } void SetColumn(const uint col) { this.m_col=(int)col; } void SetX(const uint x) { this.m_x=(int)x; } void SetY(const uint y) { this.m_y=(int)y; } void SetXY(const uint x,const uint y) { this.m_x=(int)x; this.m_y=(int)y; } void SetWidth(const uint w) { this.m_w=(int)w; } void SetHeight(const uint h) { this.m_h=(int)h; } void SetSize(const uint w,const uint h) { this.m_w=(int)w; this.m_h=(int)h; } void SetText(const string text) { this.m_text=text; } //--- Методы получения значений int Row(void) const { return this.m_row; } int Column(void) const { return this.m_col; } int X(void) const { return this.m_x; } int Y(void) const { return this.m_y; } int Width(void) const { return this.m_w; } int Height(void) const { return this.m_h; } string Text(void) const { return this.m_text; } //--- Выводит текст, записанный в свойствах ячейки на канвас, указатель на который передан в метод void TextOut(CCanvas *canvas, const int x_shift, const int y_shift, const color bg_color=clrNONE, const uint flags=0, const uint alignment=0) { if(canvas==NULL) return; //--- Запомним текущие флаги шрифта uint flags_prev=canvas.FontFlagsGet(); //--- Установим цвет фона uint clr=(bg_color==clrNONE ? 0x00FFFFFF : ::ColorToARGB(bg_color)); //--- Зальём установленным цветом фона ячейку (сотрём прошлую надпись) canvas.FillRectangle(this.m_x+1, this.m_y+1, this.m_x+this.m_w-1, this.m_y+this.m_h-1, clr); //--- Установим флаги шрифта canvas.FontFlagsSet(flags); //--- Выведем текст в ячейку canvas.TextOut(this.m_x+x_shift, this.m_y+y_shift, this.m_text, ::ColorToARGB(this.m_fore_color), alignment); //--- Возвращаем ранее запомненные флаги шрифта и обновляем канвас canvas.FontFlagsSet(flags_prev); canvas.Update(false); } //--- Виртуальный метод сравнения двух объектов
Al final del listado de clases, escribiremos una nueva clase de gestión de tablas:
//+------------------------------------------------------------------+ //| Класс управления таблицами | //+------------------------------------------------------------------+ class CTableDataControl : public CTableData { protected: uchar m_alpha; color m_fore_color; //--- Преобразует RGB в color color RGBToColor(const double r,const double g,const double b) const; //--- Записывает в переменные значения компонентов RGB void ColorToRGB(const color clr,double &r,double &g,double &b); //--- Возвращает составляющую цвета (1) Red, (2) Green, (3) Blue double GetR(const color clr) { return clr&0xff ; } double GetG(const color clr) { return(clr>>8)&0xff; } double GetB(const color clr) { return(clr>>16)&0xff; } //--- Возвращает новый цвет color NewColor(color base_color, int shift_red, int shift_green, int shift_blue); public: //--- Возвращает указатель на себя CTableDataControl*Get(void) { return &this; } //--- (1) Устанавливает, (2) возвращает прозрачность void SetAlpha(const uchar alpha) { this.m_alpha=alpha; } uchar Alpha(void) const { return this.m_alpha; } //--- Рисует (1) фоновую сетку, (2) с автоматическим размером ячеек void DrawGrid(CCanvas *canvas,const int x,const int y,const uint header_h,const uint rows,const uint columns,const uint row_size,const uint col_size, const color line_color=clrNONE,bool alternating_color=true); void DrawGridAutoFill(CCanvas *canvas,const uint border,const uint header_h,const uint rows,const uint columns,const color line_color=clrNONE,bool alternating_color=true); //--- Выводит (1) текстовое сообщение, (2) закрашенный прямоугольник в указанные координаты void DrawText(CCanvas *canvas,const string text,const int x,const int y,const color clr=clrNONE,const uint align=0,const int width=WRONG_VALUE,const int height=WRONG_VALUE); void DrawRectangleFill(CCanvas *canvas,const int x,const int y,const int width,const int height,const color clr,const uchar alpha); //--- Конструкторы/Деструктор CTableDataControl (const uint id) : CTableData(id), m_fore_color(clrDimGray), m_alpha(255) {} CTableDataControl (void) : m_alpha(255) {} ~CTableDataControl (void) {} }; //+------------------------------------------------------------------+ //| Рисует фоновую сетку | //+------------------------------------------------------------------+ void CTableDataControl::DrawGrid(CCanvas *canvas,const int x,const int y,const uint header_h,const uint rows,const uint columns,const uint row_size,const uint col_size, const color line_color=clrNONE,bool alternating_color=true) { //--- Очищаем все списки объекта табличных данных (удаляем ячейки из строк и все строки) this.Clear(); //--- Высота строки не может быть меньше 2 int row_h=int(row_size<2 ? 2 : row_size); //--- Ширина столбца не может быть меньше 2 int col_w=int(col_size<2 ? 2 : col_size); //--- Левая координата (X1) таблицы int x1=x; //--- Рассчитываем координату X2 (справа) в зависимости от количества столбцов и их ширины int x2=x1+col_w*int(columns>0 ? columns : 1); //--- Координата Y1 находится под областью заголовка панели int y1=(int)header_h+y; //--- Рассчитываем координату Y2 (снизу) в зависимости от количества строк и их высоты int y2=y1+row_h*int(rows>0 ? rows : 1); //--- Устанавливаем координаты таблицы this.SetCoords(x1,y1-header_h,x2,y2-header_h); //--- Получаем цвет линий сетки таблицы, либо по умолчанию, либо переданный в метод color clr=(line_color==clrNONE ? C'200,200,200' : line_color); //--- Рисуем рамку таблицы canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha)); //--- В цикле по строкам таблицы for(int i=0;i<(int)rows;i++) { //--- рассчитываем координату Y очередной горизонтальной линии сетки (координата Y очередной строки таблицы) int row_y=y1+row_h*i; //--- если передан флаг "чередующихся" цветов строк и строка чётная if(alternating_color && i%2==0) { //--- осветляем цвет фона таблицы и рисуем фоновый прямоугольник color new_color=this.NewColor(clr,45,45,45); canvas.FillRectangle(x1+1,row_y+1,x2-1,row_y+row_h-1,::ColorToARGB(new_color,this.m_alpha)); } //--- Рисуем горизонтальную линию сетки таблицы canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha)); //--- Создаём новый объект строки таблицы CTableRow *row_obj=new CTableRow(i); if(row_obj==NULL) { ::PrintFormat("%s: Failed to create table row object at index %lu",(string)__FUNCTION__,i); continue; } //--- Добавляем его в список строк объекта табличных данных //--- (если добавить объект не удалось - удаляем созданный объект) if(!this.AddRow(row_obj)) delete row_obj; //--- Устанавливаем в созданном объекте-строке его координату Y с учётом смещения от заголовка панели row_obj.SetY(row_y-header_h); } //--- В цикле по столбцам таблицы for(int i=0;i<(int)columns;i++) { //--- рассчитываем координату X очередной вертикальной линии сетки (координата X очередного столбца таблицы) int col_x=x1+col_w*i; //--- Если линия сетки вышла за пределы панели - прерываем цикл if(x1==1 && col_x>=x1+canvas.Width()-2) break; //--- Рисуем вертикальную линию сетки таблицы canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha)); //--- Получаем из объекта табличных данных количество созданных строк int total=this.RowsTotal(); //--- В цикле по строкам таблицы for(int j=0;j<total;j++) { //--- получаем очередную строку CTableRow *row=this.GetRow(j); if(row==NULL) continue; //--- Создаём новую ячейку таблицы CTableCell *cell=new CTableCell(row.Row(),i); if(cell==NULL) { ::PrintFormat("%s: Failed to create table cell object at index %lu",(string)__FUNCTION__,i); continue; } //--- Добавляем созданную ячейку в строку //--- (если добавить объект не удалось - удаляем созданный объект) if(!row.AddCell(cell)) { delete cell; continue; } //--- Устанавливаем в созданном объекте-ячейке его координату X и координату Y из объекта-строки cell.SetXY(col_x,row.Y()); cell.SetSize(col_w, row_h); } } //--- Обновляем канвас без перерисовки графика canvas.Update(false); } //+------------------------------------------------------------------+ //| Рисует фоновую сетку с автоматическим размером ячеек | //+------------------------------------------------------------------+ void CTableDataControl::DrawGridAutoFill(CCanvas *canvas,const uint border,const uint header_h,const uint rows,const uint columns,const color line_color=clrNONE,bool alternating_color=true) { //--- Координата X1 (левая) таблицы int x1=(int)border; //--- Координата X2 (правая) таблицы int x2=canvas.Width()-(int)border-1; //--- Координата Y1 (верхняя) таблицы int y1=int(header_h+border-1); //--- Координата Y2 (нижняя) таблицы int y2=canvas.Height()-(int)border-1; //--- Устанавливаем координаты таблицы this.SetCoords(x1,y1,x2,y2); //--- Получаем цвет линий сетки таблицы, либо по умолчанию, либо переданный в метод color clr=(line_color==clrNONE ? C'200,200,200' : line_color); //--- Если отступ от края панели больше нуля - рисуем рамку таблицы //--- иначе - рамкой таблицы выступает рамка панели if(border>0) canvas.Rectangle(x1,y1,x2,y2,::ColorToARGB(clr,this.m_alpha)); //--- Высота всей сетки таблицы int greed_h=y2-y1; //--- Рассчитываем высоту строки в зависимости от высоты таблицы и количества строк int row_h=(int)::round((double)greed_h/(double)rows); //--- В цикле по количеству строк for(int i=0;i<(int)rows;i++) { //--- рассчитываем координату Y очередной горизонтальной линии сетки (координата Y очередной строки таблицы) int row_y=y1+row_h*i; //--- если передан флаг "чередующихся" цветов строк и строка чётная if(alternating_color && i%2==0) { //--- осветляем цвет фона таблицы и рисуем фоновый прямоугольник color new_color=this.NewColor(clr,45,45,45); canvas.FillRectangle(x1+1,row_y+1,x2-1,row_y+row_h-1,::ColorToARGB(new_color,this.m_alpha)); } //--- Рисуем горизонтальную линию сетки таблицы canvas.Line(x1,row_y,x2,row_y,::ColorToARGB(clr,this.m_alpha)); //--- Создаём новый объект строки таблицы CTableRow *row_obj=new CTableRow(i); if(row_obj==NULL) { ::PrintFormat("%s: Failed to create table row object at index %lu",(string)__FUNCTION__,i); continue; } //--- Добавляем его в список строк объекта табличных данных //--- (если добавить объект не удалось - удаляем созданный объект) if(!this.AddRow(row_obj)) delete row_obj; //--- Устанавливаем в созданном объекте-строке его координату Y с учётом смещения от заголовка панели row_obj.SetY(row_y-header_h); } //--- Ширина сетки таблицы int greed_w=x2-x1; //--- Рассчитываем ширину столбца в зависимости от ширины таблицы и количества столбцов int col_w=(int)::round((double)greed_w/(double)columns); //--- В цикле по столбцам таблицы for(int i=0;i<(int)columns;i++) { //--- рассчитываем координату X очередной вертикальной линии сетки (координата X очередного столбца таблицы) int col_x=x1+col_w*i; //--- Если это не самая первая вертикальная линия - рисуем её //--- (первой вертикальной линией выступает либо рамка таблицы, либо рамка панели) if(i>0) canvas.Line(col_x,y1,col_x,y2,::ColorToARGB(clr,this.m_alpha)); //--- Получаем из объекта табличных данных количество созданных строк int total=this.RowsTotal(); //--- В цикле по строкам таблицы for(int j=0;j<total;j++) { //--- получаем очередную строку CTableRow *row=this.GetRow(j); if(row==NULL) continue; //--- Создаём новую ячейку таблицы CTableCell *cell=new CTableCell(row.Row(),i); if(cell==NULL) { ::PrintFormat("%s: Failed to create table cell object at index %lu",(string)__FUNCTION__,i); continue; } //--- Добавляем созданную ячейку в строку //--- (если добавить объект не удалось - удаляем созданный объект) if(!row.AddCell(cell)) { delete cell; continue; } //--- Устанавливаем в созданном объекте-ячейке его координату X и координату Y из объекта-строки cell.SetXY(col_x,row.Y()); cell.SetSize(col_w, row_h); } } //--- Обновляем канвас без перерисовки графика canvas.Update(false); } //+------------------------------------------------------------------+ //| Возвращает цвет с новой цветовой составляющей | //+------------------------------------------------------------------+ color CTableDataControl::NewColor(color base_color, int shift_red, int shift_green, int shift_blue) { double clR=0, clG=0, clB=0; this.ColorToRGB(base_color,clR,clG,clB); double clRn=(clR+shift_red < 0 ? 0 : clR+shift_red > 255 ? 255 : clR+shift_red); double clGn=(clG+shift_green< 0 ? 0 : clG+shift_green> 255 ? 255 : clG+shift_green); double clBn=(clB+shift_blue < 0 ? 0 : clB+shift_blue > 255 ? 255 : clB+shift_blue); return this.RGBToColor(clRn,clGn,clBn); } //+------------------------------------------------------------------+ //| Преобразует RGB в color | //+------------------------------------------------------------------+ color CTableDataControl::RGBToColor(const double r,const double g,const double b) const { int int_r=(int)::round(r); int int_g=(int)::round(g); int int_b=(int)::round(b); int clr=0; clr=int_b; clr<<=8; clr|=int_g; clr<<=8; clr|=int_r; //--- return (color)clr; } //+------------------------------------------------------------------+ //| Получение значений компонентов RGB | //+------------------------------------------------------------------+ void CTableDataControl::ColorToRGB(const color clr,double &r,double &g,double &b) { r=GetR(clr); g=GetG(clr); b=GetB(clr); } //+------------------------------------------------------------------+ //| Выводит текстовое сообщение в указанные координаты | //+------------------------------------------------------------------+ void CTableDataControl::DrawText(CCanvas *canvas,const string text,const int x,const int y,const color clr=clrNONE,const uint align=0,const int width=WRONG_VALUE,const int height=WRONG_VALUE) { //--- Объявим переменные для записи в них ширины и высоты текста int w=width; int h=height; //--- Если ширина и высота текста, переданные в метод, имеют нулевые значения, //--- то полностью очищается всё пространство канваса прозрачным цветом if(width==0 && height==0) canvas.Erase(0x00FFFFFF); //--- Иначе else { //--- Если переданные ширина и высота имеют значения по умолчанию (-1) - получаем из текста его ширину и высоту if(width==WRONG_VALUE && height==WRONG_VALUE) canvas.TextSize(text,w,h); //--- иначе, else { //--- если ширина, переданная в метод, имеет значение по умолчанию (-1) - получаем ширину из текста, либо //--- если ширина, переданная в метод, имеет значение больше нуля - используем переданную в метод ширину, либо //--- если ширина, переданная в метод, имеет нулевое значение, используем значение 1 для ширины w=(width ==WRONG_VALUE ? canvas.TextWidth(text) : width>0 ? width : 1); //--- если высота, переданная в метод, имеет значение по умолчанию (-1) - получаем высоту из текста, либо //--- если высота, переданная в метод, имеет значение больше нуля - используем переданную в метод высоту, либо //--- если высота, переданная в метод, имеет нулевое значение, используем значение 1 для высоты h=(height==WRONG_VALUE ? canvas.TextHeight(text) : height>0 ? height : 1); } //--- Заполняем пространство по указанным координатам и полученной шириной и высотой прозрачным цветом (стираем прошлую запись) canvas.FillRectangle(x,y,x+w,y+h,0x00FFFFFF); } //--- Выводим текст на очищенное от прошлого текста место и обновляем рабочую область без перерисовки экрана canvas.TextOut(x,y,text,::ColorToARGB(clr==clrNONE ? this.m_fore_color : clr),align); canvas.Update(false); } //+------------------------------------------------------------------+ //| Выводит закрашенный прямоугольник в указанные координаты | //+------------------------------------------------------------------+ void CTableDataControl::DrawRectangleFill(CCanvas *canvas,const int x,const int y,const int width,const int height,const color clr,const uchar alpha) { canvas.FillRectangle(x,y,x+width,y+height,::ColorToARGB(clr,alpha)); canvas.Update(); } //+------------------------------------------------------------------+
Esta clase contiene métodos cuyo principio describimos en el artículo "Cómo crear un panel informativo para mostrar datos en indicadores y asesores" en la sección que describe el panel de información. En el artículo anterior, los métodos pertenecían al objeto de panel. Aquí se colocarán en una clase separada heredada de la clase de tabla.
Todos los objetos de datos de tablas aquí serán del tipo de clase CTableDataControl: un objeto de gestión de tablas que le permitirá gestionar tablas rápidamente.
Echemos ahora un vistazo a lo que aquel artículo de hace tiempo sugería que descargáramos y conectáramos al asesor experto:
Y el último "plato fuerte" de nuestro programa será el trabajo con los resultados de la optimización. Si antes un tráder tenía que preparar los datos, cargarlos y procesarlos en otro lugar para procesar los resultados, ahora puede hacerlo "sin salir de la caja": durante la propia optimización. Para demostrar esta función, necesitaremos unos cuantos archivos de inclusión que implementen los ejemplos más sencillos de dicho procesamiento.
Así, rellenaremos los archivos adjuntos al artículo con la extensión MQH en la carpeta MQL5/Include. Luego tomaremos cualquier experto e insertaremos un bloque como este al final:
//--- подключим код для работы с результатами оптимизации #include <FrameGenerator.mqh> //--- генератор фреймов CFrameGenerator fg; //+------------------------------------------------------------------+ //| Tester function | //+------------------------------------------------------------------+ double OnTester() { //--- тут нужно вставить свою функцию для вычисления критерия оптимизации double TesterCritetia=MathAbs(TesterStatistics(STAT_SHARPE_RATIO)*TesterStatistics(STAT_PROFIT)); TesterCritetia=TesterStatistics(STAT_PROFIT)>0?TesterCritetia:(-TesterCritetia); //--- вызываем на каждом окончании тестирования и передаем в качестве параметра критерий оптимизации fg.OnTester(TesterCritetia); //--- return(TesterCritetia); } //+------------------------------------------------------------------+ //| TesterInit function | //+------------------------------------------------------------------+ void OnTesterInit() { //--- подготавливаем график для отображения графиков баланса fg.OnTesterInit(3); //параметр задает количество линий баланса на графике } //+------------------------------------------------------------------+ //| TesterPass function | //+------------------------------------------------------------------+ void OnTesterPass() { //--- обрабатываем полученные результаты тестирования и выводим графику fg.OnTesterPass(); } //+------------------------------------------------------------------+ //| TesterDeinit function | //+------------------------------------------------------------------+ void OnTesterDeinit() { //--- завершение оптимизации fg.OnTesterDeinit(); } //+------------------------------------------------------------------+ //| Обработка событий на графике | //+------------------------------------------------------------------+ void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- запускает воспроизведение фреймов по окончании оптимизации при нажатии на шапке fg.OnChartEvent(id,lparam,dparam,sparam,100); // 100 - это пауза в ms между кадрами } //+------------------------------------------------------------------+
Para el ejemplo tomaremos Moving Averages.mq5, un asesor incluido en el paquete estándar. Después pegaremos el código y guardaremos el asesor experto con el nombre Moving Averages With Frames.mq5. Y compilaremos y ejecutaremos la optimización.
A continuación, iremos al final del artículo y veremos los archivos adjuntos. Hay cuatro archivos con la extensión *.mqh. Ahora los cargaremos y analizaremos:
- specialchart.mqh (7.61 KB) - clase especial de gráfico en la que se dibujan las líneas de balance de cada pasada del simulador y las líneas de balance cuando se reproduce el proceso de optimización completado;
- colourprogressbar.mqh (4.86 KB) - clase de barra de progreso que muestra el proceso de optimización, llenándose de barras de colores cuando sucede la optimización. El color verde es para las series rentables, el rojo, para las series no rentables, se encuentra en la parte inferior del gráfico especial;
- simpletable.mqh (10.74 KB) - clase de tabla simple que muestra los datos de cada pasada de optimización: el resultado obtenido y los valores de los ajustes del asesor experto con los que el asesor experto se ha ejecutado en este pasada. Las dos tablas se encuentran a la izquierda de los cuadros de gráficos especiales;
- framegenerator.mqh (14.88 KB) - clase para el intercambio de datos entre el agente de pruebas y el terminal y la muestra de información en un gráfico especial. Es la clase principal para implementar la optimización visual.
Basándonos en los conocimientos adquiridos, hemos decidido crear (1) una clase de barra de progreso, (2) una clase de gráfico especial y (3) una clase de visor de frames. Ya tenemos una clase de tablas (4), cargada en la carpeta del futuro asesor experto y ligeramente modificada.
Tendremos que hacer una clase pequeña más, la clase de frame (5). ¿Para qué? Seleccionaremos y mostraremos gráficos de las tres mejores pasadas para cada uno de los cuatro criterios de optimización: Ratio de Sharpe, Rentabilidad Total, Rentabilidad y Factor de Recuperación. Será conveniente hacer esto si tenemos una lista de objetos creada usando como base la clase de array dinámico de punteros a instancias de la clase CObject y sus descendientes de la Biblioteca Estándar. Bastará con ordenar la lista por el criterio deseado, y todos los objetos de la lista se clasificarán según el valor de la propiedad del criterio seleccionado. El objeto con el valor máximo de parámetro estará al final de la lista. Solo queda encontrar dos que tengan un valor de propiedad menor que el objeto encontrado anteriormente. Los métodos para tal búsqueda ya están todos implementados en la clase mencionada.
La clase de barra de progreso, la clase de gráfico especial y la clase de visor de frames se crean sobre la base de los códigos descargados del artículo: simplemente mire cómo se hace allí y cree el suyo propio sobre esta base, ajustando, eliminando lo innecesario y añadiendo algo necesario. Echemos un vistazo a los códigos resultantes; si quiere, puede compararlos con los del artículo antiguo: el archivo con los ficheros antiguos se adjuntará al final del artículo.
Escribiremos todas las clases en un solo archivo. Lo crearemos (si no lo hemos hecho ya) en la carpeta \MQL5\Experts\FrameViewer\FrameViewer.mqh y empezaremos a llenarlo.
Conectaremos los archivos de las clases y bibliotecas necesarias al archivo creado y definiremos algunas macrosustituciones:
//+------------------------------------------------------------------+ //| FrameViewer.mqh | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #include "Controls.mqh" // Классы контроллов #include "Table.mqh" // Класс таблиц #include <Arrays\ArrayDouble.mqh> // Массив вещественных данных #define CELL_W 128 // Ширина ячеек таблицы #define CELL_H 19 // Высота ячейки таблицы #define BUTT_RES_W CELL_W+30 // Ширина кнопки выбора результата оптимизации #define DATA_COUNT 8 // Количество данных #define FRAME_ID 1 // Идентификатор фреймов #define TABLE_OPT_STAT_ID 1 // Идентификатор таблицы статистики на вкладке оптимизации #define TABLE_OPT_INP_ID 2 // Идентификатор таблицы входных параметров на вкладке оптимизации
Casi todos los objetos gráficos sobre los que se va a dibujar tienen algunos objetos del tipo CCanvas. Uno puede servir de sustrato sobre el que se colocan los otros dos: el primero se utiliza para dibujar la imagen de fondo, mientras que el segundo se utiliza para dibujar lo que se va a dibujar encima del fondo. Para los objetos cuyos métodos están diseñados para dibujar, transmitiremos a estos métodos el puntero al objeto canvas deseado, sobre el que el método dibujará.
Como hay mucho código de clase, y cada clase y sus métodos se han comentados con detalle, no describiremos todo paso a paso aquí. Vamos a ver los códigos de las clases y métodos; para ello, haremos un breve repaso sobre el código proporcionado.
Entonces, la clase de barra de progreso:
//+------------------------------------------------------------------+ //| Класс прогресс-бара, рисующий двумя цветами | //+------------------------------------------------------------------+ class CColorProgressBar :public CObject { private: CCanvas *m_background; // Указатель на объект класса CCanvas для рисования на фоне CCanvas *m_foreground; // Указатель на объект класса CCanvas для рисования на переднем плане CRect m_bound; // Координаты и размеры рабочей области color m_good_color, m_bad_color; // Цвета прибыльной и убыточной серий color m_back_color, m_fore_color; // Цвета фона и рамки bool m_passes[]; // Количество обработанных проходов int m_last_index; // Номер последнего прохода public: //--- Конструктор/деструктор CColorProgressBar(void); ~CColorProgressBar(void){}; //--- Устанавливает указатель на канвас void SetCanvas(CCanvas *background, CCanvas *foreground) { if(background==NULL) { ::Print(__FUNCTION__, ": Error. Background is NULL"); return; } if(foreground==NULL) { ::Print(__FUNCTION__, ": Error. Foreground is NULL"); return; } this.m_background=background; this.m_foreground=foreground; } //--- Устанавливает координаты и размеры рабочей области на канвасе void SetBound(const int x1, const int y1, const int x2, const int y2) { this.m_bound.SetBound(x1, y1, x2, y2); } //--- Возврат координат границ прямоугольной области int X1(void) const { return this.m_bound.left; } int Y1(void) const { return this.m_bound.top; } int X2(void) const { return this.m_bound.right; } int Y2(void) const { return this.m_bound.bottom; } //--- Установка цвета фона и рамки void SetBackColor(const color clr) { this.m_back_color=clr; } void SetForeColor(const color clr) { this.m_fore_color=clr; } //--- Возврат цвета фона и рамки color BackColor(void) const { return this.m_back_color; } color ForeColor(void) const { return this.m_fore_color; } //--- Сбрасывает счетчик в ноль void Reset(void) { this.m_last_index=0; } //--- Добавляет результат для отрисовки полоски в прогресс-баре void AddResult(bool good, const bool chart_redraw); //--- Обновляет прогресс-бар на графике void Update(const bool chart_redraw); }; //+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ CColorProgressBar::CColorProgressBar() : m_last_index(0), m_good_color(clrSeaGreen), m_bad_color(clrLightPink) { //--- Зададим размер массива проходов с запасом ::ArrayResize(this.m_passes, 5000, 1000); ::ArrayInitialize(this.m_passes, 0); } //+------------------------------------------------------------------+ //| Добавление результата | //+------------------------------------------------------------------+ void CColorProgressBar::AddResult(bool good, const bool chart_redraw) { this.m_passes[this.m_last_index]=good; //--- Добавим еще одну вертикальную черту нужного цвета в прогресс-бар this.m_foreground.LineVertical(this.X1()+1+this.m_last_index, this.Y1()+1, this.Y2()-1, ::ColorToARGB(good ? this.m_good_color : this.m_bad_color)); //--- Обновление на графике this.m_foreground.Update(chart_redraw); //--- Обновление индекса this.m_last_index++; if(this.m_last_index>=this.m_bound.Width()-1) this.m_last_index=0; } //+------------------------------------------------------------------+ //| Обновление прогресс-бара на графике | //+------------------------------------------------------------------+ void CColorProgressBar::Update(const bool chart_redraw) { //--- Зальем фон цветом фона this.m_background.FillRectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_back_color)); //--- Нарисуем рамку this.m_background.Rectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_fore_color)); //--- Обновим чарт this.m_background.Update(chart_redraw); }
La clase no tiene sus propios objetos canvas para dibujar. Para especificar el objeto canvas sobre el que dibujar, existe un método al que se transmitirá el puntero a un canvas existente, y se asignará a variables de clase. Este es el lienzo en el que se dibujarán los métodos de la clase. Aquí hay dos objetos: para dibujar el fondo de la barra de progreso, y para dibujar en primer plano sobre el fondo dibujado. El lienzo será un objeto CCanvas de la clase especial de gráfico sobre el que se dibujará esta barra de progreso.
Clase para dibujar gráficos estadísticos y tablas de resultados de optimización y ajustes del asesor experto:
//+------------------------------------------------------------------+ //| Класс для отрисовки графиков статистики и таблиц | //| результатов оптимизации и параметров настройки советника | //+------------------------------------------------------------------+ class CStatChart: public CObject { private: color m_back_color; // Цвет фона color m_fore_color; // Цвет рамки int m_line_width; // Толщина линии в пискелях int m_lines; // Количество линий на графике CArrayDouble m_seria[]; // Массивы для хранения значений графика bool m_profitseria[]; // Прибыльная серия или нет int m_lastseria_index; // Индекс свежей линии на графике color m_profit_color; // Цвет прибыльной серии color m_loss_color; // Цвет убыточной серии color m_selected_color; // Цвет выбранной лучшей серии protected: CCanvas *m_background; // Указатель на объект класса CCanvas для рисования на фоне CCanvas *m_foreground; // Указатель на объект класса CCanvas для рисования на переднем плане CRect m_bound_chart; // Рабочая область графика CRect m_bound_head; // Рабочая область заголовка чарта CColorProgressBar m_progress_bar; // Прогресс-бар CButton m_button_replay; // Кнопка воспроизведения CButtonSwitch m_button_res; // Кнопка выбора одного из трёх лучших результатов int m_tab_id; // Идентификатор вкладки public: //--- Конструктор/деструктор CStatChart() : m_lastseria_index(0), m_profit_color(clrForestGreen), m_loss_color(clrOrangeRed), m_selected_color(clrDodgerBlue), m_tab_id(0) {}; ~CStatChart() { this.m_background=NULL; this.m_foreground=NULL; } //--- Устанавливает указатель на канвас void SetCanvas(CCanvas *background, CCanvas *foreground) { if(background==NULL) { ::Print(__FUNCTION__, ": Error. Background is NULL"); return; } if(foreground==NULL) { ::Print(__FUNCTION__, ": Error. Foreground is NULL"); return; } this.m_background=background; this.m_foreground=foreground; this.m_progress_bar.SetCanvas(background, foreground); } //--- Устанавливает координаты и размеры рабочей области чарта и прогресс-бара на канвасе void SetChartBounds(const int x1, const int y1, const int x2, const int y2) { this.m_bound_chart.SetBound(x1, y1, x2, y2); this.SetBoundHeader(x1, y1-CELL_H, x2, y1); this.m_progress_bar.SetBound(x1, y2-CELL_H, x2, y2); } //--- Устанавливает координаты и размеры заголовка чарта на канвасе void SetBoundHeader(const int x1, const int y1, const int x2, const int y2) { this.m_bound_head.SetBound(x1, y1, x2, y2); } //--- Возвращает указатель на (1) себя, (2) прогресс-бар CStatChart *Get(void) { return &this; } CColorProgressBar*GetProgressBar(void) { return(&this.m_progress_bar); } //--- Установка/возврат идентификатора вкладки void SetTabID(const int id) { this.m_tab_id=id; } int TabID(void) const { return this.m_tab_id; } //--- Возврат координат границ прямоугольной области чарта int X1(void) const { return this.m_bound_chart.left; } int Y1(void) const { return this.m_bound_chart.top; } int X2(void) const { return this.m_bound_chart.right; } int Y2(void) const { return this.m_bound_chart.bottom; } //--- Возврат координат границ прямоугольной области заголовка int HeaderX1(void) const { return this.m_bound_head.left; } int HeaderY1(void) const { return this.m_bound_head.top; } int HeaderX2(void) const { return this.m_bound_head.right; } int HeaderY2(void) const { return this.m_bound_head.bottom; } //--- Возврат координат границ прямоугольной области прогресс-бара int ProgressBarX1(void) const { return this.m_progress_bar.X1(); } int ProgressBarY1(void) const { return this.m_progress_bar.Y1(); } int ProgressBarX2(void) const { return this.m_progress_bar.X2(); } int ProgressBarY2(void) const { return this.m_progress_bar.Y2(); } //--- Возвращает указатель на кнопку (1) воспроизведения, (2) выбора результата (3) худшего, (4) среднего, (5) лучшего результата CButton *ButtonReplay(void) { return(&this.m_button_replay); } CButtonSwitch *ButtonResult(void) { return(&this.m_button_res); } CButtonTriggered *ButtonResultMin(void) { return(this.m_button_res.GetButton(0)); } CButtonTriggered *ButtonResultMid(void) { return(this.m_button_res.GetButton(1)); } CButtonTriggered *ButtonResultMax(void) { return(this.m_button_res.GetButton(2)); } //--- (1) Скрывает, (2) показывает, (3) переносит на передний план кнопку вывбора результатов bool ButtonsResultHide(void) { return(this.m_button_res.Hide()); } bool ButtonsResultShow(void) { return(this.m_button_res.Show()); } bool ButtonsResultBringToTop(void) { return(this.m_button_res.BringToTop()); } //--- Создаёт кнопку воспроизведения bool CreateButtonReplay(void) { if(this.m_background==NULL) { ::PrintFormat("%s: Background is not assigned (use SetCanvas() function first)"); return false; } string text="Optimization Completed: Click to Replay"; int w=this.m_background.TextWidth(text); //--- Левая верхняя координата кнопки CPoint cp=this.m_bound_head.CenterPoint(); int x=cp.x-w/2; int y=this.Y1()+this.m_bound_head.top-2; //--- Создаём кнопу и устанавливаем для неё новые цвета, скрываем созданную кнопку if(!this.m_button_replay.Create(::StringFormat("Tab%d_ButtonReplay", this.m_tab_id), text, x, y, w, CELL_H-1)) return false; this.m_button_replay.SetDefaultColors(COLOR_BACKGROUND, STATE_OFF, C'144,238,144', C'144,228,144', C'144,218,144', clrSilver); this.m_button_replay.SetDefaultColors(COLOR_BORDER, STATE_OFF, C'144,238,144', C'144,228,144', C'144,218,144', clrSilver); this.m_button_replay.SetDefaultColors(COLOR_FOREGROUND, STATE_OFF, clrBlack, clrBlack, clrBlack, clrGray); this.m_button_replay.ResetUsedColors(STATE_OFF); this.m_button_replay.Draw(false); this.m_button_replay.Hide(); return true; } //--- Создаёт кнопку выбора результатов bool CreateButtonResults(void) { if(this.m_background==NULL) { ::PrintFormat("%s: Background is not assigned (use SetCanvas() function first)"); return false; } //--- Левая верхняя координата кнопки int x=this.m_bound_head.left+1; int y=this.m_progress_bar.Y1()+CELL_H+2; int w=BUTT_RES_W; //--- Создаём кнопу и устанавливаем для неё новые цвета, скрываем созданную кнопку if(!this.m_button_res.Create(::StringFormat("Tab%u_ButtonRes",this.m_tab_id), "", x, y, w, CELL_H-1)) return false; string text[3]={"Worst result of the top 3", "Average result of the top 3", "Best result of the top 3"}; if(!this.m_button_res.AddNewButton(text, w)) return false; this.m_button_res.GetButton(0).SetDefaultColors(COLOR_BORDER, STATE_OFF, C'228,228,228', C'228,228,228', C'228,228,228', clrSilver); this.m_button_res.GetButton(0).ResetUsedColors(STATE_OFF); this.m_button_res.GetButton(1).SetDefaultColors(COLOR_BORDER, STATE_OFF, C'228,228,228', C'228,228,228', C'228,228,228', clrSilver); this.m_button_res.GetButton(1).ResetUsedColors(STATE_OFF); this.m_button_res.GetButton(2).SetDefaultColors(COLOR_BORDER, STATE_OFF, C'228,228,228', C'228,228,228', C'228,228,228', clrSilver); this.m_button_res.GetButton(2).ResetUsedColors(STATE_OFF); this.m_button_res.Draw(false); this.m_button_res.Hide(); return true; } //--- Устанавливает цвет фона void SetBackColor(const color clr) { this.m_back_color=clr; this.m_progress_bar.SetBackColor(clr); } //--- Устанавливает цвет рамки void SetForeColor(const color clr) { this.m_fore_color=clr; this.m_progress_bar.SetForeColor(clr); } //--- Задаёт количество линий на графике void SetLines(const int num) { this.m_lines=num; ::ArrayResize(this.m_seria, num); ::ArrayResize(this.m_profitseria, num); } //--- Установка цвета (1) прибыльной, (2) убыточной, (3) выбранной серии void SetProfitColorLine(const color clr) { this.m_profit_color=clr; } void SetLossColorLine(const color clr) { this.m_loss_color=clr; } void SetSelectedLineColor(const color clr) { this.m_selected_color=clr; } //--- Обновление объекта на экране void Update(color clr, const int line_width, const bool chart_redraw); //--- Добавление данных из массива void AddSeria(const double &array[], bool profit); //--- Рисует график void Draw(const int seria_index, color clr, const int line_width, const bool chart_redraw); //--- Рисует линию в привычных координатах (слева-направо, снизу-вверх) void Line(int x1, int y1, int x2, int y2, uint col, int size); //--- Получение макс. и мин. значения в серии double MaxValue(const int seria_index); double MinValue(const int seria_index); //--- Обработчик событий void OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam) { //--- Если кнопка воспроизведения не скрыта - вызываем её обработчик событий if(!this.m_button_replay.IsHidden()) this.m_button_replay.OnChartEvent(id, lparam, dparam, sparam); //--- Если кнопка выбора результата не скрыта - вызываем её обработчик событий if(!this.m_button_res.IsHidden()) this.m_button_res.OnChartEvent(id, lparam, dparam, sparam); } };
La clase dibujará en el lienzo especificado (fondo y primer plano) las tablas con los parámetros y los resultados de las pruebas, los gráficos de las pasadas, la barra de progreso y los botones para iniciar la reproducción del proceso de optimización completado y la selección de los mejores resultados según los criterios de optimización.
Tenga en cuenta que las clases discutidas aquí usan la estructura CRect para especificar los límites de la zona rectangular del lienzo dentro de la cual se encuentra el objeto o zona rastreada.
La estructura se describe en el archivo \MQL5\Include\Controls\Rect.mqh y sirve como una cómoda herramienta para especificar los límites de una zona rectangular con elementos importantes en el interior. Por ejemplo, podemos limitar la zona en el lienzo para rastrear el cursor del ratón dentro de ella, o podemos especificar el tamaño del rectángulo delimitador al tamaño completo del lienzo. En este caso, toda la zona del objeto completo estará disponible para la interacción con el cursor. La estructura ha creado métodos que retornan las coordenadas de los límites de la zona rectangular. Podemos establecer los límites y obtener sus valores de varias maneras: todo dependerá de las necesidades y la estructura de los objetos. También se implementan los métodos para mover y desplazar la zona rectangular. En general, es una herramienta muy útil para especificar los límites de una zona que necesita ser rastreada de alguna manera.
En las clases consideradas, estas zonas son necesarias para interactuar con el cursor del ratón e indicar dónde se encuentran los objetos en el lienzo.
Método para actualizar el gráfico:
//+------------------------------------------------------------------+ //| Обновление чарта | //+------------------------------------------------------------------+ void CStatChart::Update(color clr, const int line_width, const bool chart_redraw) { //--- Если канвас для фона или переднего плана не установлен - уходим if(this.m_background==NULL || this.m_foreground==NULL) return; //--- StatChart зальем фон this.m_background.FillRectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_back_color)); //--- StatChart нарисуем рамку this.m_background.Rectangle(this.X1(), this.Y1(), this.X2(), this.Y2(), ::ColorToARGB(this.m_fore_color)); //--- ProgressBar зальем фон и нарисуем рамку this.m_progress_bar.Update(false); //--- Отрисуем каждую серию на 80% доступной площади чарта по вертикали и горизонтали for(int i=0; i<this.m_lines; i++) { //--- Если цвет задан отсутствующим - используем цвета прибыльной и убыточной серий if(clr==clrNONE) { clr=this.m_loss_color; if(this.m_profitseria[i]) clr=this.m_profit_color; } //--- иначе - используем цвет, заданный для выбранной линии else clr=this.m_selected_color; //--- Рисуем график результатов оптимизации this.Draw(i, clr, line_width, false); } //--- Обновим оба канваса this.m_background.Update(false); this.m_foreground.Update(chart_redraw); }
La zona rectangular del lienzo, destinada a dibujar gráficos de pasadas, se borrará, y sobre ella se dibujarán la línea de balance y la barra de progreso.
Método que añade una nueva serie de datos para ser dibujada en el gráfico:
//+------------------------------------------------------------------+ //| Добавляет новую серию данных для отрисовки на графике | //+------------------------------------------------------------------+ void CStatChart::AddSeria(const double &array[], bool profit) { //--- Добавляем массив в серию номер m_lastseria_index this.m_seria[this.m_lastseria_index].Resize(0); this.m_seria[this.m_lastseria_index].AddArray(array); this.m_profitseria[this.m_lastseria_index]=profit; //--- Отслеживаем индекс последней линии (не используется в данный момент) this.m_lastseria_index++; if(this.m_lastseria_index>=this.m_lines) this.m_lastseria_index=0; }
Cada nueva pasada del optimizador, su array de datos, debe introducirse en una array de series, que es lo que hace este método.
Métodos para obtener el valor máximo y mínimo de una serie indicada en un conjunto de pasadas del optimizador:
//+------------------------------------------------------------------+ //| Получение максимального значения указанной серии | //+------------------------------------------------------------------+ double CStatChart::MaxValue(const int seria_index) { double res=this.m_seria[seria_index].At(0); int total=this.m_seria[seria_index].Total(); //--- Переберем массив и сравним каждые две соседние серии for(int i=1; i<total; i++) { if(this.m_seria[seria_index].At(i)>res) res=this.m_seria[seria_index].At(i); } //--- результат return res; } //+------------------------------------------------------------------+ //| Получение минимального значения указанной серии | //+------------------------------------------------------------------+ double CStatChart::MinValue(const int seria_index) { double res=this.m_seria[seria_index].At(0);; int total=this.m_seria[seria_index].Total(); //--- Переберем массив и сравним каждые две соседние серии for(int i=1; i<total; i++) { if(this.m_seria[seria_index].At(i)<res) res=this.m_seria[seria_index].At(i); } //--- результат return res; }
Para colocar las series de pasadas del optimizador relativamente en el centro del gráfico especial, deberemos conocer los valores máximo y mínimo de las series de pasadas. A continuación, podemos utilizar estos valores para calcular las coordenadas relativas de la línea en el gráfico, de modo que la línea se ajuste al 80% del espacio del gráfico asignado para dibujar los gráficos de balance de las pasadas del optimizador.
Método para trazar una línea de balance en un gráfico:
//+------------------------------------------------------------------+ //| Перегрузка базовой функции рисования | //+------------------------------------------------------------------+ void CStatChart::Line(int x1, int y1, int x2, int y2, uint col, int size) { //--- Если канвас не задан - уходим if(this.m_foreground==NULL) return; //--- Так как ось Y перевернута, то нужно перевернуть y1 и y2 int y1_adj=this.m_bound_chart.Height()-CELL_H-y1; int y2_adj=this.m_bound_chart.Height()-CELL_H-y2; //--- Рисуем сглаженную линию //--- Если толщина линии меньше 3, то рисуем линию с использованием алгоритма сглаживания Ву //--- (при толщине 1 и 2 в методе LineThick() вызывается метод LineWu()), //--- иначе - рисуем сглаженную линию заданной толщины при помощи LineThick this.m_foreground.LineThick(x1, y1_adj, x2, y2_adj,::ColorToARGB(col), (size<1 ? 1 : size), STYLE_SOLID, LINE_END_ROUND); }
Este es un método sobrecargado del método homónimo de la clase CCanvas. Las coordenadas del gráfico parten de la esquina superior izquierda. Y las coordenadas habituales de los gráficos de balance parten de la parte inferior izquierda.
Este método invierte las coordenadas Y en pantalla para dibujar una línea de balance sin invertir usando los valores de los puntos de balance del array.
Método que dibuja líneas de balance en un gráfico:
//+------------------------------------------------------------------+ //| Отрисовка линии баланса на графике | //+------------------------------------------------------------------+ void CStatChart::Draw(const int seria_index, color clr, const int line_width, const bool chart_redraw) { //--- Если канвас не задан - уходим if(this.m_foreground==NULL) return; //--- Готовим коэффициенты для перевода значений в пиксели double min=this.MaxValue(seria_index); double max=this.MinValue(seria_index); double size=this.m_seria[seria_index].Total(); //--- Отступы от края графика double x_indent=this.m_bound_chart.Width()*0.05; double y_indent=this.m_bound_chart.Height()*0.05; //--- Вычислим коэффициенты double k_y=(max-min)/(this.m_bound_chart.Height()-2*CELL_H-2*y_indent); double k_x=(size)/(this.m_bound_chart.Width()-2*x_indent); //--- Постоянные double start_x=this.m_bound_chart.left+x_indent; double start_y=this.m_bound_chart.bottom-2*CELL_H*2-y_indent; //--- Теперь рисуем ломанную линию проходя по всем точкам серии for(int i=1; i<size; i++) { //--- переводим значения в пиксели int x1=(int)((i-0)/k_x+start_x); // номер значения откладываем на горизонтали int y1=(int)(start_y-(m_seria[seria_index].At(i)-min)/k_y); // по вертикали int x2=(int)((i-1-0)/k_x+start_x);// номер значения откладываем на горизонтали int y2=(int)(start_y-(m_seria[seria_index].At(i-1)-min)/k_y); // по вертикали //--- Выводим линию от предыдущей точки к текущей this.Line(x1, y1, x2, y2, clr, line_width); } //--- Обновление канваса с перерисовкой графика (если флаг установлен) this.m_foreground.Update(chart_redraw); }
Aquí calculamos las coordenadas requeridas de la línea de balance en el gráfico (dentro de la zona del gráfico diseñada para dibujar gráficos de balance), y en un ciclo a través del array de la serie especificada dibujaremos líneas entre todos los puntos de balance registrados en el array.
Clase de datos del frame:
//+------------------------------------------------------------------+ //| Перечисления | //+------------------------------------------------------------------+ enum ENUM_FRAME_PROP // Свойства фрейма { FRAME_PROP_PASS_NUM, // Номер прохода FRAME_PROP_SHARPE_RATIO, // Результат Sharpe Ratio FRAME_PROP_NET_PROFIT, // Результат Net Profit FRAME_PROP_PROFIT_FACTOR, // Результат Profit Factor FRAME_PROP_RECOVERY_FACTOR, // Результат Recovery Factor }; //+------------------------------------------------------------------+ //| Класс данных фрейма | //+------------------------------------------------------------------+ class CFrameData : public CObject { protected: ulong m_pass; // Номер прохода double m_sharpe_ratio; // Коэффициент Шарпа double m_net_profit; // Общая прибыль double m_profit_factor; // Доходность double m_recovery_factor; // Фактор восстановления public: //--- Установка свойств фрейма (результатов прохода) void SetPass(const ulong pass) { this.m_pass=pass; } void SetSharpeRatio(const double value) { this.m_sharpe_ratio=value; } void SetNetProfit(const double value) { this.m_net_profit=value; } void SetProfitFactor(const double value) { this.m_profit_factor=value; } void SetRecoveryFactor(const double value) { this.m_recovery_factor=value; } //--- Возврат свойств фрейма (результатов прохода) ulong Pass(void) const { return this.m_pass; } double SharpeRatio(void) const { return this.m_sharpe_ratio; } double NetProfit(void) const { return this.m_net_profit; } double ProfitFactor(void) const { return this.m_profit_factor; } double RecoveryFactor(void) const { return this.m_recovery_factor; } //--- Описание свойств string PassDescription(void) const { return ::StringFormat("Pass: %I64u", this.m_pass); } string SharpeRatioDescription(void) const { return ::StringFormat("Sharpe Ratio: %.2f", this.m_sharpe_ratio); } string NetProfitDescription(void) const { return ::StringFormat("Net Profit: %.2f", this.m_net_profit); } string ProfitFactorDescription(void) const { return ::StringFormat("Profit Factor: %.2f", this.m_profit_factor); } string RecoveryFactorDescription(void) const { return ::StringFormat("Recovery Factor: %.2f", this.m_recovery_factor); } //--- Вывод в журнал свойств фрейма void Print(void) { ::PrintFormat("Frame %s:", this.PassDescription()); ::PrintFormat(" - %s", this.SharpeRatioDescription()); ::PrintFormat(" - %s", this.NetProfitDescription()); ::PrintFormat(" - %s", this.ProfitFactorDescription()); ::PrintFormat(" - %s", this.RecoveryFactorDescription()); } //--- Метод сравнения двух объектов virtual int Compare(const CObject *node,const int mode=0) const { //--- Вещественные значения сравниваем как двухзначные const CFrameData *obj=node; switch(mode) { case FRAME_PROP_SHARPE_RATIO : return(::NormalizeDouble(this.SharpeRatio(),2) > ::NormalizeDouble(obj.SharpeRatio(),2) ? 1 : ::NormalizeDouble(this.SharpeRatio(),2) < ::NormalizeDouble(obj.SharpeRatio(),2) ? -1 : 0); case FRAME_PROP_NET_PROFIT : return(::NormalizeDouble(this.NetProfit(),2) > ::NormalizeDouble(obj.NetProfit(),2) ? 1 : ::NormalizeDouble(this.NetProfit(),2) < ::NormalizeDouble(obj.NetProfit(),2) ? -1 : 0); case FRAME_PROP_PROFIT_FACTOR : return(::NormalizeDouble(this.ProfitFactor(),2) > ::NormalizeDouble(obj.ProfitFactor(),2) ? 1 : ::NormalizeDouble(this.ProfitFactor(),2) < ::NormalizeDouble(obj.ProfitFactor(),2) ? -1 : 0); case FRAME_PROP_RECOVERY_FACTOR : return(::NormalizeDouble(this.RecoveryFactor(),2)> ::NormalizeDouble(obj.RecoveryFactor(),2) ? 1 : ::NormalizeDouble(this.RecoveryFactor(),2)< ::NormalizeDouble(obj.RecoveryFactor(),2) ? -1 : 0); //---FRAME_PROP_PASS_NUM default : return(this.Pass()>obj.Pass() ? 1 : this.Pass()<obj.Pass() ? -1 : 0); } } //--- Конструкторы/деструктор CFrameData (const ulong pass, const double sharpe_ratio, const double net_profit, const double profit_factor, const double recovery_factor) : m_pass(pass), m_sharpe_ratio(sharpe_ratio), m_net_profit(net_profit), m_profit_factor(profit_factor), m_recovery_factor(recovery_factor) {} CFrameData (void) : m_pass(0), m_sharpe_ratio(0), m_net_profit(0), m_profit_factor(0), m_recovery_factor(0) {} ~CFrameData (void) {} };
Una vez finalizada cada pasada del optimizador, se enviará un frame al terminal. Este contendrá todos los datos que se han obtenido al completar esta pasada. Para acceder a los datos de cualquier pasada, deberemos buscar el frame con el número requerido en un ciclo a través de todas los frames obtenidos y obtener sus datos. Esto no resulta óptimo en absoluto. Necesitamos poder acceder rápidamente a los datos de la pasada deseada, y poder ordenar todos los pasadas por la propiedad especificada, ya que tendremos que seleccionar las tres mejores pasadas en función de uno de los cuatro criterios de optimización.
La salida sería guardar las pasadas en la caché. Para ello, necesitamos una clase de objeto de frame. Tras completar cada pasada y enviar el frame al terminal, deberemos crear un objeto de frame, rellenar sus propiedades con los datos del frame de prueba recibido y colocar el objeto de frame en la lista. Entonces, una vez finalizado el proceso de optimización y recuperados todos los frames, tendremos copias de todos los frames de la lista de estos. Y ahora podremos clasificar esta lista de frames según las propiedades deseadas y recuperar rápidamente de ella los datos del frame deseado.
Cabe señalar que en el método Compare() teníamos que hacer una comparación de números reales no comparando la diferencia normalizada con cero, sino dos números normalizados entre sí. ¿Y por qué?
Para comparar dos números reales, podemos tomar distintos caminos. El primero consiste en comparar números no normalizados. Primero comparamos el operador ternario sobre "mayor que", luego sobre "menor que", y al final, lo que quedará es "igual a". O podemos comparar la diferencia normalizada de dos números con cero. Pero aquí hemos tenido que normalizar ambos números a dos dígitos y comparar dichos valores.
El problema es que en el terminal, en la tabla de resultados, se muestran números de dos dígitos en los resultados de la optimización. Sin embargo, internamente estos números no están normalizados a dos dígitos. Es decir, la representación de dos dígitos de los resultados solo se refleja en la tabla de resultados. Y si la tabla tiene valores como 1,09 y 1,08, puede que en realidad no sea así. Puede encontrar números como 1.085686399864 o 1.081254322375. Ambas cifras se redondean a 1,09 y 1,08 en la tabla. Y lo que podemos encontrar al comparar es que ambos números se redondean mediante normalización al mismo valor. Y si no se normaliza, puede que no haya un valor de 1,09. Y esto provocaría una búsqueda incorrecta de las mejores pasadas.
La solución consiste en normalizar ambos números a dos dígitos y luego comparar sus valores redondeados.
Clase de visor de frames:
//+------------------------------------------------------------------+ //| Класс просмотровщика фреймов | //+------------------------------------------------------------------+ class CFrameViewer : public CObject { private: int m_w; // Ширина графика int m_h; // Высота графика color m_selected_color; // Цвет выбранной серии из трёх лучших uint m_line_width; // Толщина линии выбранной серии из трёх лучших bool m_completed; // Флаг завершения оптимизации CFrameData m_frame_tmp; // Объект фрейм для поиска по свойству CArrayObj m_list_frames; // Список фреймов CTabControl m_tab_control; // Элемент управления Tab Control //--- Объявляем объекты вкладок на элементе управления Tab Control //--- Вкладка 0 (Optimization) элемента управления Tab Control CTableDataControl m_table_inp_0; // Таблица параметров оптимизации на вкладке 0 CTableDataControl m_table_stat_0; // Таблица результатов оптимизации на вкладке 0 CStatChart m_chart_stat_0; // График оптимизации на вкладке 0 CColorProgressBar*m_progress_bar; // Прогресс-бар на графике оптимизации на вкладке 0 //--- Вкладка 1 (Sharpe Ratio) элемента управления Tab Control CTableDataControl m_table_inp_1; // Таблица параметров оптимизации на вкладке 1 CTableDataControl m_table_stat_1; // Таблица результатов оптимизации на вкладке 1 CStatChart m_chart_stat_1; // График результатов оптимизации на вкладке 1 //--- Вкладка 2 (Net Profit) элемента управления Tab Control CTableDataControl m_table_inp_2; // Таблица параметров оптимизации на вкладке 2 CTableDataControl m_table_stat_2; // Таблица результатов оптимизации на вкладке 2 CStatChart m_chart_stat_2; // График результатов оптимизации на вкладке 2 //--- Вкладка 3 (Profit Factor) элемента управления Tab Control CTableDataControl m_table_inp_3; // Таблица параметров оптимизации на вкладке 3 CTableDataControl m_table_stat_3; // Таблица результатов оптимизации на вкладке 3 CStatChart m_chart_stat_3; // График результатов оптимизации на вкладке 3 //--- Вкладка 4 (Recovery Factor) элемента управления Tab Control CTableDataControl m_table_inp_4; // Таблица параметров оптимизации на вкладке 4 CTableDataControl m_table_stat_4; // Таблица результатов оптимизации на вкладке 4 CStatChart m_chart_stat_4; // График результатов оптимизации на вкладке 4 protected: //--- Возвращает указатель на таблицу параметров оптимизации по индексу вкладки CTableDataControl*GetTableInputs(const uint tab_id) { switch(tab_id) { case 0 : return this.m_table_inp_0.Get(); case 1 : return this.m_table_inp_1.Get(); case 2 : return this.m_table_inp_2.Get(); case 3 : return this.m_table_inp_3.Get(); case 4 : return this.m_table_inp_4.Get(); default: return NULL; } } //--- Возвращает указатель на таблицу результатов оптимизации по индексу вкладки CTableDataControl*GetTableStats(const uint tab_id) { switch(tab_id) { case 0 : return this.m_table_stat_0.Get(); case 1 : return this.m_table_stat_1.Get(); case 2 : return this.m_table_stat_2.Get(); case 3 : return this.m_table_stat_3.Get(); case 4 : return this.m_table_stat_4.Get(); default: return NULL; } } //--- Возвращает указатель на график результатов оптимизации по индексу вкладки CStatChart *GetChartStats(const uint tab_id) { switch(tab_id) { case 0 : return this.m_chart_stat_0.Get(); case 1 : return this.m_chart_stat_1.Get(); case 2 : return this.m_chart_stat_2.Get(); case 3 : return this.m_chart_stat_3.Get(); case 4 : return this.m_chart_stat_4.Get(); default: return NULL; } } //--- Добавляет объект фрейм в список bool AddFrame(CFrameData *frame) { if(frame==NULL) { ::PrintFormat("%s: Error: Empty object passed",__FUNCTION__); return false; } this.m_frame_tmp.SetPass(frame.Pass()); this.m_list_frames.Sort(FRAME_PROP_PASS_NUM); int index=this.m_list_frames.Search(frame); if(index>WRONG_VALUE) return false; return this.m_list_frames.Add(frame); } //--- Рисует таблицу статистики оптимизации на указанной вкладке void TableStatDraw(const uint tab_id, const int x, const int y, const int w, const int h, const bool chart_redraw); //--- Рисует таблицу входных параметров оптимизации на указанной вкладке void TableInpDraw(const uint tab_id, const int x, const int y, const int w, const int h, const uint rows, const bool chart_redraw); //--- Рисует график оптимизации на указанной вкладке void ChartOptDraw(const uint tab_id, const bool opt_completed, const bool chart_redraw); //--- Рисует таблицы данных и график оптимизации void DrawDataChart(const uint tab_id); //--- Рисует графики трёх лучших проходов по критерию оптимизации void DrawBestFrameData(const uint tab_id, const int res_index); //--- Управляет отображением управляющих объектов на графиках оптимизации void ControlObjectsView(const uint tab_id); //--- Повторное проигрывание фреймов после окончания оптимизации void ReplayFrames(const int delay_ms); //--- Получение данных текущего фрейма и вывод их на указанной вкладке в таблицу и на график результатов оптимизации bool DrawFrameData(const uint tab_id, const string text, color clr, const uint line_width, ulong &pass, string ¶ms[], uint &par_count, double &data[]); //--- Выводит данные указанного фрейма на график оптимизации bool DrawFrameDataByPass(const uint tab_id, const ulong pass_num, const string text, color clr, const uint line_width, double &data[]); //--- Заполняет массив индексами фреймов трёх лучших проходов для указанного критерия оптимизации (по индексу вкладки) bool FillArrayBestFrames(const uint tab_id, ulong &array_passes[]); //--- Выводит на графики результатов оптимизации на каждой вкладке по три лучших прохода void DrawBestFrameDataAll(void); //--- Ищет и возвращает указатель на объект фрейма, со значением свойства меньше образца CFrameData *FrameSearchLess(CFrameData *frame, const int mode); public: //--- Установка толщины выбранной линии void SetSelectedLineWidth(const uint width) { this.m_line_width=width; } //--- Установка цвета прибыльной серии void SetProfitColorLine(const color clr) { int total=this.m_tab_control.TabsTotal(); for(int i=1; i<total; i++) { CStatChart *chart=this.GetChartStats(i); if(chart!=NULL) chart.SetProfitColorLine(clr); } } //--- Установка цвета убыточной серии void SetLossColorLine(const color clr) { int total=this.m_tab_control.TabsTotal(); for(int i=1; i<total; i++) { CStatChart *chart=this.GetChartStats(i); if(chart!=NULL) chart.SetLossColorLine(clr); } } //--- Установка цвета выбранной серии void SetSelectedLineColor(const color clr) { int total=this.m_tab_control.TabsTotal(); for(int i=1; i<total; i++) { CStatChart *chart=this.GetChartStats(i); if(chart!=NULL) chart.SetSelectedLineColor(clr); } } //--- Обработчики событий тестера стратегий void OnTester(const double OnTesterValue); int OnTesterInit(const int lines, const int selected_line_width, const color selected_line_color); void OnTesterPass(void); void OnTesterDeinit(void); //--- Обработчики событий графика void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam,const int delay_ms); protected: //--- Обработчик (1) смены вкладки элемента Tab Control, (2) выбора кнопки переключателя Button Switch void OnTabSwitchEvent(const int tab_id); void OnButtonSwitchEvent(const int tab_id, const uint butt_id); public: //--- Конструктор/деструктор CFrameViewer(void); ~CFrameViewer(void){ this.m_list_frames.Clear(); } };
Ya sabemos exactamente cuántas pestañas habrá y qué elementos se colocarán en cada una. Así que aquí no se crearán nuevos objetos, sino simplemente instancias declaradas de los objetos requeridos para cada pestaña, los métodos para acceder a ellos, y los métodos para el funcionamiento de la clase.
Constructor:
//+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ CFrameViewer::CFrameViewer(void) : m_completed(false), m_progress_bar(NULL), m_selected_color(clrDodgerBlue), m_line_width(1) { //--- Размеры окна графика this.m_w=(int)::ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); this.m_h=(int)::ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Получаем указатель на прогресс-бар из объекта чарта статистики this.m_progress_bar=this.m_chart_stat_0.GetProgressBar(); this.m_list_frames.Clear(); }
En el constructor obtendremos y recordaremos la anchura y altura del gráfico donde se ejecuta el asesor experto, encontraremos y registraremos el puntero a la barra de progreso y borraremos la lista de frames.
Al iniciar la optimización, antes de que comience, deberemos preparar un gráfico en el que se ejecutará una copia del asesor experto en el modo frame en el terminal de cliente. El gráfico se desprenderá del terminal, sobre él -en su tamaño completo- se colocará el elemento Tab Control, y en sus pestañas se situarán otros elementos, en los que se mostrarán los gráficas de balance de pasadas y los botones de control.
Todo esto deberá hacerse en el manejador OnTesterInit(). Para ello, la clase proporcionará los manejadores homónimos, que en el asesor experto se iniciarán desde una instancia de la clase CFrameViewer.
Manejador OnTesterInit:
//+------------------------------------------------------------------+ //| Должна вызываться в обработчике эксперта OnTesterInit() | //+------------------------------------------------------------------+ int CFrameViewer::OnTesterInit(const int lines, const int selected_line_width, const color selected_line_color) { //--- Идентификатор графика с экспертом, работающем во Frame-режиме long chart_id=::ChartID(); //--- Готовим плавающий график для рисования таблиц статистики и линий баланса ::ResetLastError(); if(!::ChartSetInteger(chart_id, CHART_SHOW, false)) { ::PrintFormat("%s: ChartSetInteger() failed. Error %d",__FUNCTION__, GetLastError()); return INIT_FAILED; } if(!::ChartSetInteger(chart_id, CHART_IS_DOCKED, false)) { ::PrintFormat("%s: ChartSetInteger() failed. Error %d",__FUNCTION__, GetLastError()); return INIT_FAILED; } //--- Очищаем график полностью от всех графических объектов ::ObjectsDeleteAll(chart_id); //--- По размерам графика создаём элемент управления Tab Control с пятью вкладками int w=(int)::ChartGetInteger(chart_id, CHART_WIDTH_IN_PIXELS); int h=(int)::ChartGetInteger(chart_id, CHART_HEIGHT_IN_PIXELS); if(this.m_tab_control.Create("TabControl", "", 0, 0, w, h)) { //--- Если элемент управления создан успешно - добавляем к нему пять вкладок bool res=true; for(int i=0; i<5; i++) { string tab_text=(i==1 ? "Sharpe Ratio" : i==2 ? "Net Profit" : i==3 ? "Profit Factor" : i==4 ? "Recovery Factor" : "Optimization"); res &=this.m_tab_control.AddTab(i, tab_text); } if(!res) { ::PrintFormat("%s: Errors occurred while adding tabs to the Tab Control",__FUNCTION__); return INIT_FAILED; } } else { Print("Tab Control creation failed"); return INIT_FAILED; } //--- Объекты CCanvas рабочей области вкладки 0 (Optimization) для рисования фоновых изображений и текста CCanvas *tab0_background=this.m_tab_control.GetTabBackground(0); CCanvas *tab0_foreground=this.m_tab_control.GetTabForeground(0); //--- Объекты CCanvas рабочей области вкладки 1 (Sharpe Ratio) для рисования фоновых изображений и текста CCanvas *tab1_background=this.m_tab_control.GetTabBackground(1); CCanvas *tab1_foreground=this.m_tab_control.GetTabForeground(1); //--- Объекты CCanvas рабочей области вкладки 2 (Net Profit) для рисования фоновых изображений и текста CCanvas *tab2_background=this.m_tab_control.GetTabBackground(2); CCanvas *tab2_foreground=this.m_tab_control.GetTabForeground(2); //--- Объекты CCanvas рабочей области вкладки 3 (Profit Factor) для рисования фоновых изображений и текста CCanvas *tab3_background=this.m_tab_control.GetTabBackground(3); CCanvas *tab3_foreground=this.m_tab_control.GetTabForeground(3); //--- Объекты CCanvas рабочей области вкладки 4 (Recovery Factor) для рисования фоновых изображений и текста CCanvas *tab4_background=this.m_tab_control.GetTabBackground(4); CCanvas *tab4_foreground=this.m_tab_control.GetTabForeground(4); //--- Устанавливаем объектам графиков статистики оптимизации идентификаторы вкладок this.m_chart_stat_0.SetTabID(0); this.m_chart_stat_1.SetTabID(1); this.m_chart_stat_2.SetTabID(2); this.m_chart_stat_3.SetTabID(3); this.m_chart_stat_4.SetTabID(4); //--- Указываем для объектов чартов статистики, что рисуем на вкладке с соответствующим индексом this.m_chart_stat_0.SetCanvas(tab0_background, tab0_foreground); this.m_chart_stat_1.SetCanvas(tab1_background, tab1_foreground); this.m_chart_stat_2.SetCanvas(tab2_background, tab2_foreground); this.m_chart_stat_3.SetCanvas(tab3_background, tab3_foreground); this.m_chart_stat_4.SetCanvas(tab4_background, tab4_foreground); //--- Устанавливаем количество серий на графиках статистики оптимизации this.m_chart_stat_0.SetLines(lines); this.m_chart_stat_1.SetLines(lines); this.m_chart_stat_2.SetLines(lines); this.m_chart_stat_3.SetLines(lines); this.m_chart_stat_4.SetLines(lines); //--- Задаём цвета фона и переднего плана графиков статистики оптимизации this.m_chart_stat_0.SetBackColor(clrIvory); this.m_chart_stat_0.SetForeColor(C'200,200,200'); this.m_chart_stat_1.SetBackColor(clrIvory); this.m_chart_stat_1.SetForeColor(C'200,200,200'); this.m_chart_stat_2.SetBackColor(clrIvory); this.m_chart_stat_2.SetForeColor(C'200,200,200'); this.m_chart_stat_3.SetBackColor(clrIvory); this.m_chart_stat_3.SetForeColor(C'200,200,200'); this.m_chart_stat_4.SetBackColor(clrIvory); this.m_chart_stat_4.SetForeColor(C'200,200,200'); //--- Установим толщину и цвет выбранной линии лучшего прохода this.SetSelectedLineWidth(selected_line_width); this.SetSelectedLineColor(selected_line_color); //--- Нарисуем на вкладке 0 (Optimization) две таблицы с результатами оптимизации и входными параметрами, //--- и окно с полосой прогресса для вывода графиков и процесса оптимизации this.TableStatDraw(0, 4, 4, CELL_W*2, CELL_H, false); this.TableInpDraw(0, 4, this.m_table_stat_0.Y2()+4, CELL_W*2, CELL_H, 0, false); this.ChartOptDraw(0, this.m_completed, true); //--- Создадим на вкладке 0 кнопку воспроизведения оптимизации if(!this.m_chart_stat_0.CreateButtonReplay()) { Print("Button Replay creation failed"); return INIT_FAILED; } //--- Нарисуем на вкладке 1 (Sharpe Ratio) две таблицы с результатами оптимизации и входными параметрами, //--- и окно для вывода графиков результатов оптимизации this.TableStatDraw(1, 4, 4, CELL_W*2, CELL_H, false); this.TableInpDraw(1, 4, this.m_table_stat_1.Y2()+4, CELL_W*2, CELL_H, 0, false); this.ChartOptDraw(1, this.m_completed, true); //--- Создадим на вкладке 1 кнопку выбора результата if(!this.m_chart_stat_1.CreateButtonResults()) { Print("Tab1: There were errors when creating the result buttons"); return INIT_FAILED; } //--- Нарисуем на вкладке 2 (Net Profit) две таблицы с результатами оптимизации и входными параметрами, //--- и окно для вывода графиков результатов оптимизации this.TableStatDraw(2, 4, 4, CELL_W*2, CELL_H, false); this.TableInpDraw(2, 4, this.m_table_stat_1.Y2()+4, CELL_W*2, CELL_H, 0, false); this.ChartOptDraw(2, this.m_completed, true); //--- Создадим на вкладке 2 кнопку выбора результата if(!this.m_chart_stat_2.CreateButtonResults()) { Print("Tab2: There were errors when creating the result buttons"); return INIT_FAILED; } //--- Нарисуем на вкладке 3 (Profit Factor) две таблицы с результатами оптимизации и входными параметрами, //--- и окно для вывода графиков результатов оптимизации this.TableStatDraw(3, 4, 4, CELL_W*2, CELL_H, false); this.TableInpDraw(3, 4, this.m_table_stat_1.Y2()+4, CELL_W*2, CELL_H, 0, false); this.ChartOptDraw(3, this.m_completed, true); //--- Создадим на вкладке 3 кнопку выбора результата if(!this.m_chart_stat_3.CreateButtonResults()) { Print("Tab3: There were errors when creating the result buttons"); return INIT_FAILED; } //--- Нарисуем на вкладке 4 (Recovery Factor) две таблицы с результатами оптимизации и входными параметрами, //--- и окно для вывода графиков результатов оптимизации this.TableStatDraw(4, 4, 4, CELL_W*2, CELL_H, false); this.TableInpDraw(4, 4, this.m_table_stat_1.Y2()+4, CELL_W*2, CELL_H, 0, false); this.ChartOptDraw(4, this.m_completed, true); //--- Создадим на вкладке 4 кнопку выбора результата if(!this.m_chart_stat_4.CreateButtonResults()) { Print("Tab4: There were errors when creating the result buttons"); return INIT_FAILED; } return INIT_SUCCEEDED; }
Aquí la creación de todos los elementos se realizará bloque a bloque. Cada bloque de código será responsable de crear algún elemento de la interfaz del programa.
Una vez finalizada la optimización, deberemos realizar algunos cambios en la interfaz creada: cambiar el color de los encabezados de los gráficos, cambiar los textos de los mismos y mostrar el botón de inicio de reproducción en la primera pestaña (con identificador 0). Todo esto deberá hacerse en el manejador OnTesterDeinit().
Manejador OnTesterDeinit:
//+------------------------------------------------------------------+ //| Должна вызываться в обработчике эксперта OnTesterDeinit() | //+------------------------------------------------------------------+ void CFrameViewer::OnTesterDeinit(void) { //--- Получаем указатели на канвас для рисования фона и переднего плана CCanvas *background=this.m_tab_control.GetTabBackground(0); CCanvas *foreground=this.m_tab_control.GetTabForeground(0); if(background==NULL || foreground==NULL) return; //--- Устанавливаем флаг завершения оптимизации this.m_completed=true; //--- Координаты заголовка графика int x1=this.m_chart_stat_0.HeaderX1(); int y1=this.m_chart_stat_0.HeaderY1(); int x2=this.m_chart_stat_0.HeaderX2(); int y2=this.m_chart_stat_0.HeaderY2(); int x=(x1+x2)/2; int y=(y1+y2)/2; //--- Перекрасим фон и сотрём текст заголовка background.FillRectangle(x1, y1, x2, y2, ::ColorToARGB(clrLightGreen)); foreground.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF); //--- Изменим текст и цвет шапки заголовка string text="Optimization Complete: Click to Replay"; foreground.FontSet("Calibri", -100, FW_BLACK); foreground.TextOut(x, y, text, ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER); background.Update(false); foreground.Update(true); //--- Получаем индекс активной вкладки и вызываем метод управления отображением управляющих объектов на графиках оптимизации int tab_selected=this.m_tab_control.GetSelectedTabID(); this.ControlObjectsView(tab_selected); //--- На каждой вкладке (1 - 4) нарисуем графики трёх лучших проходов оптимизации this.DrawBestFrameDataAll(); ::ChartRedraw(); }
Al final de cada pasada del optimizador, se generará un evento Tester que podrá procesarse en el manejador OnTester(). Luego este se iniciará en el lado de la instancia del asesor experto que se ejecuta en el agente de pruebas.
En este manejador deberemos recoger todos los datos sobre la pasada completada, formar un frame y enviarlo al terminal cliente utilizando la función FrameAdd().
Manejador OnTester:
//+------------------------------------------------------------------+ //| Готовит массив значений баланса и отправляет его во фрейме | //| Должна вызываться в эксперте в обработчике OnTester() | //+------------------------------------------------------------------+ void CFrameViewer::OnTester(const double OnTesterValue) { //--- Переменные для работы с результатами прохода double balance[]; int data_count=0; double balance_current=::TesterStatistics(STAT_INITIAL_DEPOSIT); //--- Временные переменные для работы со сделками ulong ticket=0; double profit; string symbol; long entry; //--- Запросим всю торговую историю ::ResetLastError(); if(!::HistorySelect(0, ::TimeCurrent())) { PrintFormat("%s: HistorySelect() failed. Error ",__FUNCTION__, ::GetLastError()); return; } //--- Собираем данные о сделках uint deals_total=::HistoryDealsTotal(); for(uint i=0; i<deals_total; i++) { ticket=::HistoryDealGetTicket(i); if(ticket==0) continue; symbol=::HistoryDealGetString(ticket, DEAL_SYMBOL); entry =::HistoryDealGetInteger(ticket, DEAL_ENTRY); profit=::HistoryDealGetDouble(ticket, DEAL_PROFIT); if(entry!=DEAL_ENTRY_OUT && entry!=DEAL_ENTRY_INOUT) continue; balance_current+=profit; data_count++; ::ArrayResize(balance, data_count); balance[data_count-1]=balance_current; } //--- Массив data[] для отправки данных во фрейм double data[]; ::ArrayResize(data, ::ArraySize(balance)+DATA_COUNT); ::ArrayCopy(data, balance, DATA_COUNT, 0); //--- Заполним первые DATA_COUNT значений массива результатами тестирования data[0]=::TesterStatistics(STAT_SHARPE_RATIO); // коэффициент Шарпа data[1]=::TesterStatistics(STAT_PROFIT); // чистая прибыль data[2]=::TesterStatistics(STAT_PROFIT_FACTOR); // фактор прибыльности data[3]=::TesterStatistics(STAT_RECOVERY_FACTOR); // фактор восстановления data[4]=::TesterStatistics(STAT_TRADES); // количество трейдов data[5]=::TesterStatistics(STAT_DEALS); // количество сделок data[6]=::TesterStatistics(STAT_EQUITY_DDREL_PERCENT); // максимальная просадка средств в процентах data[7]=OnTesterValue; // значение пользовательского критерия оптимизации if(data[2]==DBL_MAX) data[2]=0; //--- Создадим фрейм с данными и отправим его в терминал if(!::FrameAdd(::MQLInfoString(MQL_PROGRAM_NAME), FRAME_ID, deals_total, data)) ::PrintFormat("%s: Frame add error: ",__FUNCTION__, ::GetLastError()); }
Cuando el asesor experto en el terminal cliente recibe un frame enviado desde el agente, se generará el evento TesterPass y se gestionará en el manejador OnTesterPass().
En este manejador, tomaremos la información del frame, trazaremos el balance de este pasada en el gráfico y rellenaremos las tablas de resultados de las pruebas y los parámetros. Luego guardaremos el frame procesado en un nuevo objeto de frame y lo añadiremos a la lista de frames para trabajar con él cuando necesitemos buscar los pasadas necesarias para mostrarlas en los gráficos.
Manejador OnTesterPass:
//+------------------------------------------------------------------+ //| Получает фрейм с данными при оптимизации и отображает график | //| Должна вызываться в эксперте в обработчике OnTesterPass() | //+------------------------------------------------------------------+ void CFrameViewer::OnTesterPass(void) { //--- Переменные для работы со фреймами string name; ulong pass; long id; double value, data[]; string params[]; uint par_count; //--- Вспомогательные переменные static datetime start=::TimeLocal(); static int frame_counter=0; //--- При получении нового фрейма получаем из него данные while(!::IsStopped() && ::FrameNext(pass, name, id, value, data)) { frame_counter++; string text=::StringFormat("Frames completed (tester passes): %d in %s", frame_counter,::TimeToString(::TimeLocal()-start, TIME_MINUTES|TIME_SECONDS)); //--- Получим входные параметры эксперта, для которых сформирован фрейм, и отправим их в таблицы и на график //--- При успешном получении фрейма запишем его данные в объект фрейм и разместим его в списке if(this.DrawFrameData(0, text, clrNONE, 0, pass, params, par_count, data)) { //--- Результаты прохода тестера double sharpe_ratio=data[0]; double net_profit=data[1]; double profit_factor=data[2]; double recovery_factor=data[3]; //--- Создаём новый объект фрейм и сохраняем его в списке CFrameData *frame=new CFrameData(pass, sharpe_ratio, net_profit, profit_factor, recovery_factor); if(frame!=NULL) { if(!this.AddFrame(frame)) delete frame; } ::ChartRedraw(); } } }
Una vez finalizado el proceso de optimización, el asesor experto iniciado en el modo frame seguirá funcionando en el terminal en el gráfico flotante. Y todo el trabajo con este asesor experto se organizará dentro del manejador OnChartEvent(), ya que controlaremos los procesos necesarios utilizando los botones del gráfico y el cursor del ratón.
Manejador OnChartEvent:
//+------------------------------------------------------------------+ //| Обработка событий на графике | //+------------------------------------------------------------------+ void CFrameViewer::OnChartEvent(const int id,const long &lparam, const double &dparam,const string &sparam, const int delay_ms) { //--- Вызываем обработчики событий объекта управления вкладками и графиков результатов оптимизации this.m_tab_control.OnChartEvent(id, lparam, dparam, sparam); this.m_chart_stat_0.OnChartEvent(id, lparam, dparam, sparam); this.m_chart_stat_1.OnChartEvent(id, lparam, dparam, sparam); this.m_chart_stat_2.OnChartEvent(id, lparam, dparam, sparam); this.m_chart_stat_3.OnChartEvent(id, lparam, dparam, sparam); this.m_chart_stat_4.OnChartEvent(id, lparam, dparam, sparam); //--- Если пришло событие изменения графика if(id==CHARTEVENT_CHART_CHANGE) { //--- получим размеры графика int w=(int)::ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); int h=(int)::ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); if(w!=this.m_w || h!=this.m_h) { if(w==0 || h==0) return; //--- Изменим размер элемента управления Tab Control this.m_tab_control.Resize(w, h); //--- Получим идентификатор выбранной вкладки и нарисуем таблицы данных и график оптимизации на вкладке int tab_selected=this.m_tab_control.GetSelectedTabID(); this.DrawDataChart(tab_selected); //--- Получим указатель на кнопку-переключатель и выбранную кнопку показа результатов оптимизации CButtonSwitch *button_switch=(tab_selected>0 ? this.GetChartStats(tab_selected).ButtonResult() : NULL); uint res_index=(button_switch!=NULL ? button_switch.SelectedButton() : -1); //--- В зависимости от выбранной вкладки switch(tab_selected) { //--- вкладка 0 (Optimization) case 0 : //--- Рисуем график с линией последнего прохода и две пустые таблицы this.DrawDataChart(0); //--- Запускает воспроизведение проведённой оптимизации, //--- что останавливает работу с остальным пока длится воспроизведение //if(this.m_completed) // this.ReplayFrames(1); break; //--- вкладки 1 - 4 default: //--- Получаем индекс выбранной кнопки прохода оптимизации res_index=button_switch.SelectedButton(); //--- Рисуем график с результатами трёх лучших проходов выбранной вкладки this.DrawDataChart(tab_selected); this.DrawBestFrameData(tab_selected, -1); this.DrawBestFrameData(tab_selected, res_index); //--- На вкладке 0 рисуем график с линией последнего прохода и две пустые таблицы this.DrawDataChart(0); //--- Запускает воспроизведение проведённой оптимизации, //--- что останавливает работу с остальным пока длится воспроизведение //--- Чтобы заново нарисовать графики всех проходов, можно нажать кнопку воспроизведения //if(this.m_completed) // this.ReplayFrames(1); break; } //--- Запомним новые размеры для последующей проверки this.m_w=w; this.m_h=h; } } //--- Если процесс оптимизации не завершён - уходим if(!this.m_completed) return; //--- Если пришло пользовательское событие if(id>CHARTEVENT_CUSTOM) { //--- Если пришло событие кнопки Replay и оптимизация завершена if(sparam==this.m_chart_stat_0.ButtonReplay().Name() && this.m_completed) { //--- скроем кнопку Replay, this.m_chart_stat_0.ButtonReplay().Hide(); //--- Инициализируем график результатов оптимизации, this.ChartOptDraw(0, this.m_completed, true); //--- запустим воспроизведение, this.m_completed=false; // заблокируем, чтобы не запустить несколько раз подряд this.ReplayFrames(delay_ms); // процедура воспроизведения this.m_completed=true; // снимаем блокировку //--- После завершения воспроизведения покажем кнопку Replay и перерисуем график this.m_chart_stat_0.ButtonReplay().Show(); ::ChartRedraw(); } //--- Получаем указатели на кнопки вкладок CTabButton *tab_btn0=this.m_tab_control.GetTabButton(0); CTabButton *tab_btn1=this.m_tab_control.GetTabButton(1); CTabButton *tab_btn2=this.m_tab_control.GetTabButton(2); CTabButton *tab_btn3=this.m_tab_control.GetTabButton(3); CTabButton *tab_btn4=this.m_tab_control.GetTabButton(4); if(tab_btn0==NULL || tab_btn1==NULL || tab_btn2==NULL || tab_btn3==NULL || tab_btn4==NULL) return; //--- Получаем идентификатор выбранной вкладки int tab_selected=this.m_tab_control.GetSelectedTabID(); //--- Если пришло событие переключения на вкладку 0 if(sparam==tab_btn0.Name()) { //--- На вкладке 0 рисуем график с линией последнего прохода и две таблицы с пустыми результатами this.DrawDataChart(0); //--- Запускает воспроизведение проведённой оптимизации //--- (может долго длиться - при желании, чтобы отобразить графики, можно нажать кнопку Replay) //if(this.m_completed) // this.ReplayFrames(1); ::ChartRedraw(); return; } //--- Получаем указатель на чарт выбранной вкладки CStatChart *chart_stat=this.GetChartStats(tab_selected); if(tab_selected==0 || chart_stat==NULL) return; //--- Получаем указатели на кнопки чарта выбранной вкладки (индекс вкладки 1 - 4) CButtonTriggered *button_min=chart_stat.ButtonResultMin(); CButtonTriggered *button_mid=chart_stat.ButtonResultMid(); CButtonTriggered *button_max=chart_stat.ButtonResultMax(); if(button_min==NULL || button_mid==NULL || button_max==NULL) return; //--- Если пришло событие переключения на вкладку 1 if(sparam==tab_btn1.Name()) { //--- вызываем обработчик переключения на вкладку this.OnTabSwitchEvent(1); } //--- Если пришло событие переключения на вкладку 2 if(sparam==tab_btn2.Name()) { //--- вызываем обработчик переключения на вкладку this.OnTabSwitchEvent(2); } //--- Если пришло событие переключения на вкладку 3 if(sparam==tab_btn3.Name()) { //--- вызываем обработчик переключения на вкладку this.OnTabSwitchEvent(3); } //--- Если пришло событие переключения на вкладку 4 if(sparam==tab_btn4.Name()) { //--- вызываем обработчик переключения на вкладку this.OnTabSwitchEvent(4); } //--- Если пришло событие нажатие на кнопку минимального результата выбранной вкладки if(sparam==button_min.Name()) { //--- вызываем обработчик переключения кнопки-переключателя this.OnButtonSwitchEvent(tab_selected, 0); } //--- Если пришло событие нажатие на кнопку среднего результата выбранной вкладки if(sparam==button_mid.Name()) { //--- вызываем обработчик переключения кнопки-переключателя this.OnButtonSwitchEvent(tab_selected, 1); } //--- Если пришло событие нажатие на кнопку лучшего результата выбранной вкладки if(sparam==button_max.Name()) { //--- вызываем обработчик переключения кнопки-переключателя this.OnButtonSwitchEvent(tab_selected, 2); } } }
Los eventos de cambio de pestañas del control de pestañas y de pulsación de botones del botón de cambio se gestionarán en los manejadores personalizados correspondientes. Todas las acciones que se realizan en ellos son idénticas. La única diferencia será el ID de la pestaña. Es por eso que estos eventos están diseñados para ser procesados en sus propios manejadores.
Controlador de cambio de pestaña:
//+------------------------------------------------------------------+ //| Обработчик переключения вкладки | //+------------------------------------------------------------------+ void CFrameViewer::OnTabSwitchEvent(const int tab_id) { //--- Получаем указатель на чарт выбранной вкладки CStatChart *chart_stat=this.GetChartStats(tab_id); if(chart_stat==NULL) return; //--- Получаем указатель на кнопку-переключатель чарта выбранной вкладки CButtonSwitch *button_switch=chart_stat.ButtonResult(); if(button_switch==NULL) return; //--- Индекс нажатой кнопки uint butt_index=button_switch.SelectedButton(); //--- Инициализируем график результатов на вкладке tab_id и this.DrawDataChart(tab_id); //--- вызываем метод, контролирующий отображение управляющих элементов на всех вкладках this.ControlObjectsView(tab_id); //--- Рисуем все три лучших прохода this.DrawBestFrameData(tab_id, -1); //--- Выделяем проход, выбранный кнопкой this.DrawBestFrameData(tab_id, butt_index); }
Controlador del botón de alternancia:
//+------------------------------------------------------------------+ //| Обработчик переключения кнопки-переключателя | //+------------------------------------------------------------------+ void CFrameViewer::OnButtonSwitchEvent(const int tab_id, const uint butt_id) { //--- Инициализируем график результатов на вкладке tab_id this.DrawDataChart(tab_id); //--- Рисуем все три лучших прохода this.DrawBestFrameData(tab_id, -1); //--- Выделяем проход, выбранный кнопкой butt_id this.DrawBestFrameData(tab_id, butt_id); }
Método que dibuja las tablas de datos y un gráfico de optimización:
//+------------------------------------------------------------------+ //| Рисует таблицы данных и график оптимизации | //+------------------------------------------------------------------+ void CFrameViewer::DrawDataChart(const uint tab_id) { //--- Рисуем таблицу статистики, таблицу входных параметров и график оптимизации this.TableStatDraw(tab_id, 4, 4, CELL_W*2, CELL_H, false); this.TableInpDraw(tab_id, 4, this.GetTableStats(tab_id).Y2()+4, CELL_W*2, CELL_H, this.GetTableInputs(tab_id).RowsTotal(), false); this.ChartOptDraw(tab_id, this.m_completed, true); //--- вызываем метод, контролирующий отображение управляющих элементов на всех вкладках this.ControlObjectsView(tab_id); }
Una vez dibujados todas las tablas y gráficos, deberemos colocar correctamente los controles, ocultar los botones de las pestañas inactivas y mostrar los botones de las pestañas activas, que es lo que hace el método ControlObjectsView.
Método que gestiona la visualización de los controles en los gráficos de optimización:
//+-------------------------------------------------------------------+ //|Управляет отображением управляющих объектов на графиках оптимизации| //+-------------------------------------------------------------------+ void CFrameViewer::ControlObjectsView(const uint tab_id) { //--- Получаем индекс активной вкладки int tab_index=this.m_tab_control.GetSelectedTabID(); //--- Получаем указатель на активную вкладку и таблицу статистики оптимизации CTab *tab=this.m_tab_control.GetTab(tab_index); CTableDataControl *table_stat=this.GetTableStats(tab_index); if(tab==NULL || table_stat==NULL) return; //--- Координаты левой и правой границ заголовка графика результатов оптимизации int w=0, cpx=0, x=0, y=0; int x1=table_stat.X2()+10; int x2=tab.GetField().Right()-10; //--- В зависимости от индекса выбранной вкладки switch(tab_index) { //--- Optimization case 0 : //--- Смещаем кнопку Replay к центру заголовка w=this.m_chart_stat_0.ButtonReplay().Width(); cpx=(x1+x2)/2; x=cpx-w/2; this.m_chart_stat_0.ButtonReplay().MoveX(x); //--- Если оптимизация завершена - показываем кнопку на переднем плане if(this.m_completed) { this.m_chart_stat_0.ButtonReplay().Show(); this.m_chart_stat_0.ButtonReplay().BringToTop(); } //--- Скрываем кнопки всех остальных вкладок this.m_chart_stat_1.ButtonsResultHide(); this.m_chart_stat_2.ButtonsResultHide(); this.m_chart_stat_3.ButtonsResultHide(); this.m_chart_stat_4.ButtonsResultHide(); break; //--- Sharpe Ratio case 1 : //--- Скрываем кнопку Replay this.m_chart_stat_0.ButtonReplay().Hide(); //--- Получаем координату Y и смещаем на неё кнопку-переключатель y=this.m_chart_stat_1.ProgressBarY1()+CELL_H+2; this.m_chart_stat_1.ButtonResult().MoveY(y); //--- Кнопку-переключатель на вкладке 1 переносим на передний план, //--- а все остальные кнопки на других вкладках скрываем this.m_chart_stat_1.ButtonsResultBringToTop(); this.m_chart_stat_2.ButtonsResultHide(); this.m_chart_stat_3.ButtonsResultHide(); this.m_chart_stat_4.ButtonsResultHide(); break; //--- Net Profit case 2 : this.m_chart_stat_0.ButtonReplay().Hide(); //--- Получаем координату Y и смещаем на неё кнопку-переключатель y=this.m_chart_stat_2.ProgressBarY1()+CELL_H+2; this.m_chart_stat_2.ButtonResult().MoveY(y); //--- Кнопку-переключатель на вкладке 2 переносим на передний план, //--- а все остальные кнопки на других вкладках скрываем this.m_chart_stat_2.ButtonsResultBringToTop(); this.m_chart_stat_1.ButtonsResultHide(); this.m_chart_stat_3.ButtonsResultHide(); this.m_chart_stat_4.ButtonsResultHide(); break; //--- Profit Factor case 3 : this.m_chart_stat_0.ButtonReplay().Hide(); //--- Получаем координату Y и смещаем на неё кнопку-переключатель y=this.m_chart_stat_3.ProgressBarY1()+CELL_H+2; this.m_chart_stat_3.ButtonResult().MoveY(y); //--- Кнопку-переключатель на вкладке 3 переносим на передний план, //--- а все остальные кнопки на других вкладках скрываем this.m_chart_stat_3.ButtonsResultBringToTop(); this.m_chart_stat_1.ButtonsResultHide(); this.m_chart_stat_2.ButtonsResultHide(); this.m_chart_stat_4.ButtonsResultHide(); break; //--- Recovery Factor case 4 : this.m_chart_stat_0.ButtonReplay().Hide(); //--- Получаем координату Y и смещаем на неё кнопку-переключатель y=this.m_chart_stat_4.ProgressBarY1()+CELL_H+2; this.m_chart_stat_4.ButtonResult().MoveY(y); //--- Кнопку-переключатель на вкладке 4 переносим на передний план, //--- а все остальные кнопки на других вкладках скрываем this.m_chart_stat_4.ButtonsResultBringToTop(); this.m_chart_stat_1.ButtonsResultHide(); this.m_chart_stat_2.ButtonsResultHide(); this.m_chart_stat_3.ButtonsResultHide(); break; default: break; } //--- Перерисовываем график ::ChartRedraw(); }
Método que realiza la repetición de frames una vez finalizada la optimización:
//+------------------------------------------------------------------+ //| Повторное проигрывание фреймов после окончания оптимизации | //+------------------------------------------------------------------+ void CFrameViewer::ReplayFrames(const int delay_ms) { //--- Переменные для работы со фреймами string name; ulong pass; long id; double value, data[]; string params[]; uint par_count; //--- Счетчик фреймов int frame_counter=0; //--- Очистим счетчики прогресс-бара this.m_progress_bar.Reset(); this.m_progress_bar.Update(false); //--- Переводим указатель фреймов в начало и запускаем перебор фреймов ::FrameFirst(); while(!::IsStopped() && ::FrameNext(pass, name, id, value, data)) { //--- Увеличиваем счётчик фреймов и подготавливаем текст заголовка графика оптимизации frame_counter++; string text=::StringFormat("Playing with pause %d ms: frame %d", delay_ms, frame_counter); //--- Получаем входные параметры эксперта, для которых сформирован фрейм, данные фрейма и выводим их на график if(this.DrawFrameData(0, text, clrNONE, 0, pass, params, par_count, data)) ::ChartRedraw(); //--- Подождём delay_ms миллисекунд ::Sleep(delay_ms); } }
Todos los frames resultantes estarán disponibles para su visualización tras la optimización. Aquí, en un simple ciclo desde el primer frame, nos moveremos a través de todos los frames disponibles y mostraremos sus datos en tablas y en el gráfico.
Método que da salida a los datos de frame especificados en un gráfico de optimización:
//+------------------------------------------------------------------+ //| Выводит данные указанного фрейма на график оптимизации | //+------------------------------------------------------------------+ bool CFrameViewer::DrawFrameDataByPass(const uint tab_id, const ulong pass_num, const string text, color clr, const uint line_width, double &data[]) { //--- Переменные для работы со фреймами string name; ulong pass; long id; uint par_count; double value; string params[]; //--- Переводим указатель фреймов в начало и запускаем поиск фрейма pass_num ::FrameFirst(); while(::FrameNext(pass, name, id, value, data)) { //--- Если номер прохода соответствует искомому - //--- получаем данные фрейма и выводим их в таблицу //--- и на график на вкладке tab_id if(pass==pass_num) { if(DrawFrameData(tab_id, text, clr, line_width, pass, params, par_count, data)) return true; } } //--- Проход не найден return false; }
Como los frames disponibles tras la optimización solo pueden obtenerse en el ciclo FrameFirst() --> FrameNext(), y por métodos estándar de ninguna otra forma, aquí iteraremos en un ciclo por todos los frames disponibles en busca de aquel cuyo número de pasada necesitamos. En cuanto se encuentra el frame deseado, sus datos se muestran en el gráfico.
Básicamente, después de la optimización tenemos una lista preparada de objetos de frame, y podemos recuperar rápidamente el objeto deseado de la lista. Podemos utilizar dicho acceso al frame necesario, pero en este caso tendremos que escribir más métodos para obtener los datos del objeto de frame y del array de series, convertirlos al formato necesario y mostrarlos en el gráfico. Pero por ahora, el acceso se dejará exactamente como está en el método anterior, para reducir la cantidad de código en la clase y hacerla más fácil de entender.
Método que dibuja gráficos de las tres mejores pasadas según el criterio de optimización:
//+------------------------------------------------------------------+ //| Рисует графики трёх лучших проходов по критерию оптимизации | //+------------------------------------------------------------------+ void CFrameViewer::DrawBestFrameData(const uint tab_id, const int res_index) { //--- Если переданы некорректные идентификаторы таблицы и нажатой кнопки - уходим if(tab_id<1 || tab_id>4 || res_index>2) { ::PrintFormat("%s: Error. Incorrect table (%u) or selected button (%d) identifiers passed",__FUNCTION__, tab_id, res_index); return; } //--- Массивы для получения результатов проходов ulong array_passes[3]; double data[]; //--- Создаём текст заголовка графика проходов string res= ( tab_id==1 ? "Results by Sharpe Ratio" : tab_id==2 ? "Results by Net Profit" : tab_id==3 ? "Results by Profit Factor" : tab_id==4 ? "Results by Recovery Factor" : "" ); string text="Optimization Completed: "+res; //--- Заполняем массив array_passes индексами трёх лучших проходов this.FillArrayBestFrames(tab_id, array_passes); //--- Если индекс кнопки прохода задан отрицательным числом - if(res_index<0) { //--- выводим на график все три прохода //--- (цвет линий указывается как clrNONE для автоматического выбора цвета линий прибыльной или убыточной серий) for(int i=0; i<(int)array_passes.Size(); i++) this.DrawFrameDataByPass(tab_id, array_passes[i], text, clrNONE, 0, data); } //--- Иначе - выводим на график серию, указанную индексом нажатой кнопки (res_index), //--- цветом, заданным в m_selected_color, и толщиной, указанной в m_line_width else this.DrawFrameDataByPass(tab_id, array_passes[res_index], text, this.m_selected_color, this.m_line_width, data); }
Aquí, el array se rellenará primero con índices de los frames de las tres mejores pasadas en el método FillArrayBestFrames(), y luego se trazará la pasada deseada (o las tres).
Método que rellena un array con los índices de frame de las tres mejores pasadas para el criterio de optimización especificado:
//+------------------------------------------------------------------+ //| Заполняет массив индексами фреймов трёх лучших проходов | //| для указанного критерия оптимизации (по индексу вкладки) | //+------------------------------------------------------------------+ bool CFrameViewer::FillArrayBestFrames(const uint tab_id, ulong &array_passes[]) { //--- Очищаем переданный в метод массив индексов проходов оптимизации ::ZeroMemory(array_passes); //FRAME_PROP_PASS_NUM, // Номер прохода //FRAME_PROP_SHARPE_RATIO, // Результат Sharpe Ratio //FRAME_PROP_NET_PROFIT, // Результат Net Profit //FRAME_PROP_PROFIT_FACTOR, // Результат Profit Factor //FRAME_PROP_RECOVERY_FACTOR, // Результат Recovery Factor //--- По идентификатору вкладки будем определять свойство, по которому искать лучшие проходы оптимизации //--- Проверяем идентификатор вкладки, чтобы был в пределах от 1 до 4 if(tab_id<FRAME_PROP_SHARPE_RATIO || tab_id>FRAME_PROP_RECOVERY_FACTOR) { ::PrintFormat("%s: Error: Invalid tab ID passed (%u)",__FUNCTION__, tab_id); return false; } //--- Преобразуем идентификатор таблицы в свойство фрейма ENUM_FRAME_PROP prop=(ENUM_FRAME_PROP)tab_id; //--- Сортируем список фреймов в порядке возрастания по свойству, //--- соответствующему значению tab_id в виде перечисления ENUM_FRAME_PROP this.m_list_frames.Sort(prop); //--- После сортировки, фрейм с лучшим результатом будет находиться в конце списка //--- Получаем по индексу фрейм из списка с максимальным значением результата и int index=this.m_list_frames.Total()-1; CFrameData *frame_next=this.m_list_frames.At(index); if(frame_next==NULL) return false; //--- записываем номер прохода в последнюю ячейку массива array_passes array_passes[2]=frame_next.Pass(); //--- Теперь найдём объекты, у которых результат оптимизации по убыванию меньше найденного максимального //--- В цикле от 1 до 0 (оставшиеся ячейки массива array_passes) for(int i=1; i>=0; i--) { //--- ищем предыдущий объект со значением свойства меньше, чем у объекта frame_next frame_next=this.FrameSearchLess(frame_next, prop); //--- В очередную ячейку массива array_passes вписываем номер прохода найденного объекта //--- Если объект не найден - значит, нет объектов со значением, меньше, чем у объекта frame_next, //--- и в очередную ячейку массива array_passes в этом случае записываем его предыдущее значение array_passes[i]=(frame_next!=NULL ? frame_next.Pass() : array_passes[i+1]); } //--- Всё успешно return true; }
Toda la lógica del método está completamente descrita en los comentarios del código. Cuando finalice el método, los números de las tres mejores pasadas según el criterio de optimización se registrarán en un array de tamaño 3, correspondiente al número de la pestaña en cuyo gráfico se mostrarán los datos de dichas pasadas. El método FrameSearchLess() se utilizará para buscar los frames que tengan un valor de propiedad menor que el actual.
Método para encontrar y devolver el puntero a un objeto de frame con un valor de propiedad menor que la muestra:
//+------------------------------------------------------------------+ //| Ищет и возвращает указатель на объект фрейма, | //| со значением свойства меньше образца | //+------------------------------------------------------------------+ CFrameData *CFrameViewer::FrameSearchLess(CFrameData *frame, const int mode) { //--- В зависимости от типа свойства фрейма switch(mode) { //--- во временный объект записываем соответствующее свойство переданного в метод объекта case FRAME_PROP_SHARPE_RATIO : this.m_frame_tmp.SetSharpeRatio(frame.SharpeRatio()); break; case FRAME_PROP_NET_PROFIT : this.m_frame_tmp.SetNetProfit(frame.NetProfit()); break; case FRAME_PROP_PROFIT_FACTOR : this.m_frame_tmp.SetProfitFactor(frame.ProfitFactor()); break; case FRAME_PROP_RECOVERY_FACTOR : this.m_frame_tmp.SetRecoveryFactor(frame.RecoveryFactor()); break; default : this.m_frame_tmp.SetPass(frame.Pass()); break; } //--- Сортируем массив фреймов по указанному свойству и this.m_list_frames.Sort(mode); //--- получаем индекс ближайшего объекта с меньшим значением свойства, либо -1 int index=this.m_list_frames.SearchLess(&this.m_frame_tmp); //--- Получаем из списка объект по индексу и возвращаем указатель на него, либо NULL CFrameData *obj=this.m_list_frames.At(index); return obj; }
Se pasa un frame al método, y en la lista clasificada de frames, utilizando el método SearchLess() de la clase CArrayObj de la Biblioteca Estándar, se buscará el objeto más cercano con un valor de propiedad menor que el transmitido al método.
Método que traza las tres mejores pasadas en cada pestaña de los gráficos de resultados de optimización:
//+------------------------------------------------------------------+ //| Выводит на графики результатов оптимизации | //| на каждой вкладке по три лучших прохода | //+------------------------------------------------------------------+ void CFrameViewer::DrawBestFrameDataAll(void) { //--- В цикле по всем вкладкам от вкладки 1, рисуем графики трёх лучших проходов для каждой вкладки for(int i=1; i<this.m_tab_control.TabsTotal(); i++) this.DrawBestFrameData(i,-1); }
Método para obtener los datos del frame actual y mostrarlos en la pestaña especificada en una tabla y el gráfico de los resultados de la optimización:
//+------------------------------------------------------------------+ //| Получение данных текущего фрейма и вывод их на указанной вкладке | //| в таблицу и на график результатов оптимизации | //+------------------------------------------------------------------+ bool CFrameViewer::DrawFrameData(const uint tab_id, const string text, color clr, const uint line_width, ulong &pass, string ¶ms[], uint &par_count, double &data[]) { //--- Проверяем переданный идентификатор вкладки if(tab_id>4) { ::PrintFormat("%s: Error: Invalid tab ID passed (%u)",__FUNCTION__, tab_id); return false; } //--- Получаем указатели на используемые объекты на указанной вкладке CCanvas *foreground=this.m_tab_control.GetTabForeground(tab_id); CTableDataControl *table_stat=this.GetTableStats(tab_id); CTableDataControl *table_inp=this.GetTableInputs(tab_id); CStatChart *chart_stat=this.GetChartStats(tab_id); if(foreground==NULL || table_stat==NULL || table_inp==NULL || chart_stat==NULL) return false; //--- Получим входные параметры эксперта, для которых сформирован фрейм, данные фрейма и выведем их на график ::ResetLastError(); if(::FrameInputs(pass, params, par_count)) { //--- Нарисуем таблицу входных параметров на графике this.TableInpDraw(tab_id, 4, table_stat.Y2()+4, CELL_W*2, CELL_H, par_count, false); //--- Перебираем параметры, params[i], строка выглядит как "parameter=value" for(uint i=0; i<par_count; i++) { //--- Заполняем таблицу названиями и значениями входных параметров string array[]; //--- Расщепим строку в params[i] на две подстроки и обновим ячейки в строке таблицы параметров тестирования if(::StringSplit(params[i],'=',array)==2) { //--- Окрасим строки оптимизируемых параметров в бледно-желтый цвет, //--- недоступные для оптимизации параметры - в бледно-розовый, остальные - в цвета по умолчанию bool enable=false; double value=0, start=0, step=0, stop=0; color clr=clrMistyRose; if(::ParameterGetRange(array[0], enable, value, start, step, stop)) clr=(enable ? clrLightYellow : clrNONE); //--- Получим две ячейки таблицы по индексу параметра и выведем в них текст названия параметра и его значение CTableCell *cell_0=table_inp.GetCell(i, 0); CTableCell *cell_1=table_inp.GetCell(i, 1); if(cell_0!=NULL && cell_1!=NULL) { //--- Обновим надписи в ячейках cell_0.SetText(array[0]); cell_1.SetText(array[1]); cell_0.TextOut(foreground, 4, CELL_H/2, clr, 0, TA_VCENTER); cell_1.TextOut(foreground, 4, CELL_H/2, clr, 0, TA_VCENTER); } } } //--- Обновим таблицу статистики оптимизации //--- Строка заголовка таблицы foreground.FillRectangle(table_stat.X1()+1, 4+1, table_stat.X1()+CELL_W*2-1, 4+CELL_H-1, 0x00FFFFFF); foreground.FontSet("Calibri", -100, FW_BLACK); foreground.TextOut(4+(CELL_W*2)/2, 4+CELL_H/2, ::StringFormat("Optimization results (pass %I64u)", pass), ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER); //--- В цикле по количеству строк таблицы int total=table_stat.RowsTotal(); for(int i=0; i<total; i++) { //--- получим две ячейки текущей строки и CTableCell *cell_0=table_stat.GetCell(i, 0); CTableCell *cell_1=table_stat.GetCell(i, 1); if(cell_0!=NULL && cell_1!=NULL) { //--- обновим значения результатов прохода во второй ячейке string text="---"; switch(i) { case 0 : text=::StringFormat("%.2f", data[0]); break; // Sharpe Ratio case 1 : text=::StringFormat("%.2f", data[1]); break; // Net Profit case 2 : text=::StringFormat("%.2f", data[2]); break; // Profit Factor case 3 : text=::StringFormat("%.2f", data[3]); break; // Recovery Factor case 4 : text=::StringFormat("%.0f", data[4]); break; // Trades case 5 : text=::StringFormat("%.0f", data[5]); break; // Deals case 6 : text=::StringFormat("%.2f%%", data[6]);break; // Equity DD case 7 : text=::StringFormat("%G", data[7]); break; // OnTester() default: break; } //--- Подсветим цветом фон строки таблицы, соответствующей выбранной вкладке. //--- Остальные строки будут иметь цвет по умолчанию color clr=(tab_id>0 ? (i==tab_id-1 ? C'223,242,231' : clrNONE) : clrNONE); //--- Обновим надписи в ячейках cell_0.TextOut(foreground, 4, CELL_H/2, clr, 0, TA_VCENTER); cell_1.SetText(text); cell_1.TextOut(foreground, 4, CELL_H/2, clr, 0, TA_VCENTER); } } //--- Массив для приема значений баланса текущего фрейма double seria[]; ::ArrayCopy(seria, data, 0, DATA_COUNT, ::ArraySize(data)-DATA_COUNT); //--- Отправим массив для вывода на специальных график баланса chart_stat.AddSeria(seria, data[1]>0); //--- Обновим линии баланса на графике chart_stat.Update(clr, line_width, false); //--- Обновим прогресс бар (только для вкладки с идентификатором 0) if(tab_id==0) this.m_progress_bar.AddResult(data[1]>0, false); //--- Обновим надпись на шапке графика int x1=chart_stat.HeaderX1(); int y1=chart_stat.HeaderY1(); int x2=chart_stat.HeaderX2(); int y2=chart_stat.HeaderY2(); int x=(x1+x2)/2; int y=(y1+y2)/2; foreground.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF); foreground.FontSet("Calibri", -100, FW_BLACK); foreground.TextOut(x, y, text, ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER); foreground.Update(false); //--- Всё успешно return true; } //--- Что-то пошло не так... else PrintFormat("%s: FrameInputs() failed. Error %d",__FUNCTION__, ::GetLastError()); return false; }
El método recupera los datos del frame, rellena todas las tablas con estos datos y traza el gráfico de balance de este pasada de optimización.
Método que dibuja una tabla de estadísticas de optimización en la pestaña especificada:
//+------------------------------------------------------------------+ //| Рисует таблицу статистики оптимизации на указанной вкладке | //+------------------------------------------------------------------+ void CFrameViewer::TableStatDraw(const uint tab_id, const int x, const int y, const int w, const int h, const bool chart_redraw) { //--- Проверяем переданный идентификатор вкладки if(tab_id>4) { ::PrintFormat("%s: Error: Invalid tab ID passed (%u)",__FUNCTION__, tab_id); return; } //--- Получаем указатели на используемые объекты на указанной вкладке CCanvas *background=this.m_tab_control.GetTabBackground(tab_id); CCanvas *foreground=this.m_tab_control.GetTabForeground(tab_id); CTableDataControl *table_stat=this.GetTableStats(tab_id); if(background==NULL || foreground==NULL || table_stat==NULL) return; //--- Рисуем заголовок таблицы результатов оптимизации background.FillRectangle(x, y, x+CELL_W*2, y+CELL_H, ::ColorToARGB(C'195,209,223')); // C'180,190,230' foreground.FillRectangle(x+1, y+1, x+CELL_W*2-1, y+CELL_H-1, 0x00FFFFFF); foreground.FontSet("Calibri", -100, FW_BLACK); foreground.TextOut(x+(CELL_W*2)/2, y+CELL_H/2, "Optimization results", ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER); //--- Задаём таблице её идентификатор и рисуем сетку таблицы table_stat.SetID(TABLE_OPT_STAT_ID+10*tab_id); table_stat.DrawGrid(background, x, y+CELL_H, 0, DATA_COUNT, 2, CELL_H, CELL_W, C'200,200,200', false); //--- Нарисуем пустую таблицу результатов оптимизации - только заголовки, без значений //--- В цикле по строкам таблицы int total=table_stat.RowsTotal(); for(int row=0; row<total; row++) { //--- перебираем столбцы строк for(int col=0; col<2; col++) { //--- Получаем ячейку таблицы в текущей строке и столбце CTableCell *cell=table_stat.GetCell(row, col); //--- Определяем текст в ячейке //--- Для левой ячейки это будут заголовки результатов оптимизируемых параметров if(col%2==0) { string text="OnTester()"; switch(row) { case 0 : text="Sharpe Ratio"; break; case 1 : text="Net Profit"; break; case 2 : text="Profit Factor"; break; case 3 : text="Recovery Factor"; break; case 4 : text="Trades"; break; case 5 : text="Deals"; break; case 6 : text="Equity DD"; break; default: break; } cell.SetText(text); } //--- Для правой ячейки текст будет прочёркиванием для инициализируемой таблицы else cell.SetText(tab_id==0 ? " --- " : ""); //--- Выведем в ячейку соответствующий текст cell.TextOut(foreground, 4, CELL_H/2, clrNONE, 0, TA_VCENTER); } } //--- Обновим канвас фона и переднего плана background.Update(false); foreground.Update(chart_redraw); }
El método dibuja una tabla de resultados de optimización, rellenando solo los encabezados de fila de la tabla. Las celdas de datos se introducirán en la tabla siguiendo el método descrito anteriormente.
Método que dibuja una tabla de parámetros de optimización de entrada en la pestaña especificada:
//+------------------------------------------------------------------+ //|Рисует таблицу входных параметров оптимизации на указанной вкладке| //+------------------------------------------------------------------+ void CFrameViewer::TableInpDraw(const uint tab_id, const int x, const int y, const int w, const int h, const uint rows, const bool chart_redraw) { //--- Проверяем переданный идентификатор вкладки if(tab_id>4) { ::PrintFormat("%s: Error: Invalid tab ID passed (%u)",__FUNCTION__, tab_id); return; } //--- Получаем указатели на используемые объекты на указанной вкладке CCanvas *background=this.m_tab_control.GetTabBackground(tab_id); CCanvas *foreground=this.m_tab_control.GetTabForeground(tab_id); CTableDataControl *table_inp=this.GetTableInputs(tab_id); if(background==NULL || foreground==NULL || table_inp==NULL) return; //--- Рисуем заголовок таблицы параметров оптимизации background.FillRectangle(x, y, x+CELL_W*2, y+CELL_H, ::ColorToARGB(C'195,209,223')); foreground.FillRectangle(x+1, y+1, x+CELL_W*2-1, y+CELL_H-1, 0x00FFFFFF); foreground.FontSet("Calibri", -100, FW_BLACK); foreground.TextOut(x+(CELL_W*2)/2, y+CELL_H/2, "Input parameters", ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER); //--- Задаём таблице её идентификатор и рисуем сетку таблицы table_inp.SetID(TABLE_OPT_INP_ID+10*tab_id); table_inp.DrawGrid(background, x, y+CELL_H, 0, rows, 2, CELL_H, CELL_W, C'200,200,200', false); //--- Обновим канвас фона и переднего плана background.Update(false); foreground.Update(chart_redraw); }
Este método, al igual que el anterior, dibujará una tabla vacía de parámetros de optimización, que se rellenará con datos en el método DrawFrameData(), donde ya se conocen los parámetros con los que se ha realizado la pasada del simulador.
Método que dibuja un gráfico de optimización en la pestaña especificada:
//+------------------------------------------------------------------+ //| Рисует график оптимизации на указанной вкладке | //+------------------------------------------------------------------+ void CFrameViewer::ChartOptDraw(const uint tab_id, const bool opt_completed, const bool chart_redraw) { //--- Проверяем переданный идентификатор вкладки if(tab_id>4) { ::PrintFormat("%s: Error: Invalid tab ID passed (%u)",__FUNCTION__, tab_id); return; } //--- Получаем указатели на используемые объекты на указанной вкладке CCanvas *background=this.m_tab_control.GetTabBackground(tab_id); CCanvas *foreground=this.m_tab_control.GetTabForeground(tab_id); CTab *tab=this.m_tab_control.GetTab(tab_id); CTableDataControl *table_stat=this.GetTableStats(tab_id); CStatChart *chart_stat=this.GetChartStats(tab_id); if(background==NULL || foreground==NULL || tab==NULL || table_stat==NULL || chart_stat==NULL) return; //--- Рассчитаем координаты четырёх углов графика результатов оптимизации int x1=table_stat.X2()+10; int y1=table_stat.Y1(); int x2=tab.GetField().Right()-10; int y2=tab.GetField().Bottom()-tab.GetButton().Height()-12; //--- Проверим ограничения размеров по минимальным ширине и высоте (480 x 180) int w_min=480; if(x2-x1<w_min) x2=x1+w_min; if(y2-y1<180) y2=y1+180; //--- Установим размеры ограничивающего прямоугольника графика результатов оптимизации chart_stat.SetChartBounds(x1, y1, x2, y2); //--- Цвет и текст заголовка графика color clr=clrLightGreen; // цвет заголовка при завершении оптимизации string suff= ( tab_id==1 ? "Results by Sharpe Ratio" : tab_id==2 ? "Results by Net Profit" : tab_id==3 ? "Results by Profit Factor" : tab_id==4 ? "Results by Recovery Factor" : "Click to Replay" ); string text="Optimization Completed: "+suff; //--- Если оптимизация не завершена, укажем цвет и текст заголовка if(!opt_completed) { clr=C'195,209,223'; text=::StringFormat("Optimization%sprogress%s", (tab_id==0 ? " " : " in "), (tab_id==0 ? "" : ": Waiting ... ")); } //--- Рисуем заголовок и текст background.FillRectangle(x1, 4, x2, y1, ::ColorToARGB(clr)); foreground.FillRectangle(x1, 4, x2, y2, 0x00FFFFFF); foreground.FontSet("Calibri", -100, FW_BLACK); foreground.TextOut((x1+x2)/2, 4+CELL_H/2, text, ::ColorToARGB(clrMidnightBlue), TA_CENTER|TA_VCENTER); //--- Стираем полностью весь график результатов оптимизации background.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF); foreground.FillRectangle(x1, y1, x2, y2, 0x00FFFFFF); //--- Обновляем график оптимизации chart_stat.Update(clrNONE, 0, chart_redraw); }
El método preparará un gráfico limpio con un título, en el que se trazarán las líneas de balance de las pasadas de optimización completadas a partir de los métodos de dibujado.
Ya hemos escrito todas las clases necesarias para la optimización visual. Ahora el archivo de clase CFrameViewer se podrá conectar a cualquier asesor experto para ver el progreso de su optimización en un gráfico separado en el terminal.
Conexión de funciones al asesor experto
Veamos qué tenemos.
Tomaremos el asesor experto del paquete estándar de la ubicación \MQL5\Experts\Advisors\ExpertMAMA.mq5 y lo guardaremos en la carpeta \MQL5\Experts\FrameViewer\ ya creada con el nombre ExpertMAMA_Frames.mq5.
Todo lo que necesitaremos añadirle es la conexión del archivo de clase CFrameViewer al final del listado, declarar un objeto con el tipo de esta clase y añadir los manejadores donde queremos llamar a los manejadores homónimos de la clase creada.
La longitud de las variables de entrada del asesor experto puede acortarse ligeramente eliminando los guiones bajos ("_") de los nombres de las variables. Esto les dará más espacio para caber dentro de la anchura de las celdas de la tabla.
//+------------------------------------------------------------------+ //| ExpertMAMA.mq5 | //| Copyright 2000-2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| Include | //+------------------------------------------------------------------+ #include <Expert\Expert.mqh> #include <Expert\Signal\SignalMA.mqh> #include <Expert\Trailing\TrailingMA.mqh> #include <Expert\Money\MoneyNone.mqh> //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ //--- inputs for expert input string InpExpertTitle = "ExpertMAMA"; int Expert_MagicNumber = 12003; bool Expert_EveryTick = false; //--- inputs for signal input int InpSignalMAPeriod = 12; input int InpSignalMAShift = 6; input ENUM_MA_METHOD InpSignalMAMethod = MODE_SMA; input ENUM_APPLIED_PRICE InpSignalMAApplied = PRICE_CLOSE; //--- inputs for trailing input int InpTrailingMAPeriod = 12; input int InpTrailingMAShift = 0; input ENUM_MA_METHOD InpTrailingMAMethod = MODE_SMA; input ENUM_APPLIED_PRICE InpTrailingMAApplied= PRICE_CLOSE; //+------------------------------------------------------------------+ //| Global expert object | //+------------------------------------------------------------------+ CExpert ExtExpert; //+------------------------------------------------------------------+ //| Initialization function of the expert | //+------------------------------------------------------------------+ int OnInit(void) { //--- Initializing expert if(!ExtExpert.Init(Symbol(),Period(),Expert_EveryTick,Expert_MagicNumber)) { //--- failed printf(__FUNCTION__+": error initializing expert"); ExtExpert.Deinit(); return(-1); } //--- Creation of signal object CSignalMA *signal=new CSignalMA; if(signal==NULL) { //--- failed printf(__FUNCTION__+": error creating signal"); ExtExpert.Deinit(); return(-2); } //--- Add signal to expert (will be deleted automatically)) if(!ExtExpert.InitSignal(signal)) { //--- failed printf(__FUNCTION__+": error initializing signal"); ExtExpert.Deinit(); return(-3); } //--- Set signal parameters signal.PeriodMA(InpSignalMAPeriod); signal.Shift(InpSignalMAShift); signal.Method(InpSignalMAMethod); signal.Applied(InpSignalMAApplied); //--- Check signal parameters if(!signal.ValidationSettings()) { //--- failed printf(__FUNCTION__+": error signal parameters"); ExtExpert.Deinit(); return(-4); } //--- Creation of trailing object CTrailingMA *trailing=new CTrailingMA; if(trailing==NULL) { //--- failed printf(__FUNCTION__+": error creating trailing"); ExtExpert.Deinit(); return(-5); } //--- Add trailing to expert (will be deleted automatically)) if(!ExtExpert.InitTrailing(trailing)) { //--- failed printf(__FUNCTION__+": error initializing trailing"); ExtExpert.Deinit(); return(-6); } //--- Set trailing parameters trailing.Period(InpTrailingMAPeriod); trailing.Shift(InpTrailingMAShift); trailing.Method(InpTrailingMAMethod); trailing.Applied(InpTrailingMAApplied); //--- Check trailing parameters if(!trailing.ValidationSettings()) { //--- failed printf(__FUNCTION__+": error trailing parameters"); ExtExpert.Deinit(); return(-7); } //--- Creation of money object CMoneyNone *money=new CMoneyNone; if(money==NULL) { //--- failed printf(__FUNCTION__+": error creating money"); ExtExpert.Deinit(); return(-8); } //--- Add money to expert (will be deleted automatically)) if(!ExtExpert.InitMoney(money)) { //--- failed printf(__FUNCTION__+": error initializing money"); ExtExpert.Deinit(); return(-9); } //--- Set money parameters //--- Check money parameters if(!money.ValidationSettings()) { //--- failed printf(__FUNCTION__+": error money parameters"); ExtExpert.Deinit(); return(-10); } //--- Tuning of all necessary indicators if(!ExtExpert.InitIndicators()) { //--- failed printf(__FUNCTION__+": error initializing indicators"); ExtExpert.Deinit(); return(-11); } //--- succeed return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Deinitialization function of the expert | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { ExtExpert.Deinit(); } //+------------------------------------------------------------------+ //| Function-event handler "tick" | //+------------------------------------------------------------------+ void OnTick(void) { ExtExpert.OnTick(); } //+------------------------------------------------------------------+ //| Function-event handler "trade" | //+------------------------------------------------------------------+ void OnTrade(void) { ExtExpert.OnTrade(); } //+------------------------------------------------------------------+ //| Function-event handler "timer" | //+------------------------------------------------------------------+ void OnTimer(void) { ExtExpert.OnTimer(); } //+------------------------------------------------------------------+ //| Код, необходимый для визуализации оптимизации | //+------------------------------------------------------------------+ //--- При отладке, если во время оптимизации нажать "Стоп", то следующий запуск оптимизации продолжит незавершённые проходы с места остановки //--- Чтобы каждый новый запуск оптимизации начинался заново, определим директиву препроцессора #property tester_no_cache //--- Определим макроподстановки #define REPLAY_DELAY_MS 100 // Задержка воспроизведения оптимизации в миллисекундах #define STAT_LINES 1 // Количество отображаемых линий статистики оптимизации #define SELECTED_LINE_WD 3 // Толщина линии выбранного прохода оптимизации #define SELECTED_LINE_CLR clrDodgerBlue // Цвет линии выбранного прохода оптимизации //--- Подключим код для работы с результатами оптимизации просмотровщиком фреймов #include "FrameViewer.mqh" //--- Объявим объект просмотровщика фреймов CFrameViewer fw; //+------------------------------------------------------------------+ //| Tester function | //+------------------------------------------------------------------+ double OnTester() { //--- тут нужно вставить свою функцию для вычисления критерия оптимизации double TesterCritetia=MathAbs(TesterStatistics(STAT_SHARPE_RATIO)*TesterStatistics(STAT_PROFIT)); TesterCritetia=TesterStatistics(STAT_PROFIT)>0?TesterCritetia:(-TesterCritetia); //--- вызываем на каждом окончании тестирования и передаем в качестве параметра критерий оптимизации fw.OnTester(TesterCritetia); //--- return(TesterCritetia); } //+------------------------------------------------------------------+ //| TesterInit function | //+------------------------------------------------------------------+ void OnTesterInit() { //--- подготавливаем график для отображения линий баланса //--- STAT_LINES задает количество линий баланса на графике, //--- SELECTED_LINE_WD - толщину, SELECTED_LINE_CLR - цвет линии выбранного прохода fw.OnTesterInit(STAT_LINES, SELECTED_LINE_WD, SELECTED_LINE_CLR); } //+------------------------------------------------------------------+ //| TesterDeinit function | //+------------------------------------------------------------------+ void OnTesterDeinit() { //--- завершение оптимизации fw.OnTesterDeinit(); } //+------------------------------------------------------------------+ //| TesterPass function | //+------------------------------------------------------------------+ void OnTesterPass() { //--- обрабатываем полученные результаты тестирования и выводим графику fw.OnTesterPass(); } //+------------------------------------------------------------------+ //| Обработка событий на графике | //+------------------------------------------------------------------+ void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- запускает воспроизведение фреймов по окончании оптимизации при нажатии на шапке fw.OnChartEvent(id,lparam,dparam,sparam,REPLAY_DELAY_MS); // REPLAY_DELAY_MS - пауза в ms между кадрами воспроизведения }
Estos son todos los cambios y adiciones al asesor experto que deberemos realizar (excepto el acortamiento de los nombres de las variables) para que funcione la optimización visual.
Compilaremos el asesor experto y lo ejecutaremos para optimizarlo.
Los ajustes de optimización no son importantes para la prueba del programa en sí, vamos a establecerlos de la forma siguiente:

y a ejecutar la optimización:

Antes de iniciar el proceso de optimización, se abrirá una nueva ventana de gráfico en la que se encuentran todos los controles. Esto resulta muy útil para no tener que cambiar entre los gráficos adjuntos de resultados de optimización y el gráfico visual de optimización. Esta ventana independiente puede desplazarse fuera del terminal, o a un segundo monitor, y acceder simultáneamente a todos los gráficos de optimización.
Conclusión
Hoy hemos analizado solo un pequeño ejemplo de cómo se pueden realizar funciones adicionales para controlar el proceso de optimización. Cualquier dato obtenido de los informes del simulador o calculado de forma independiente después de cada pasada de optimización puede mostrarse en el gráfico de optimización visual. ¿Cómo puede ser su funcionalidad y su presentación visual? Eso dependerá de los gustos y necesidades de cada desarrollador, del uso de la optimización visual para obtener los resultados deseados y la usabilidad de los datos obtenidos. Llegados a este punto, resulta importante que hayamos visto ejemplos concretos de cómo se puede hacer y utilizar todo lo que necesitamos para nosotros.
Adjuntos al artículo encontrará todos los archivos tratados en el artículo para el autoaprendizaje. En el fichero Old_article_files.zip hallará los archivos del artículo, basados en la información usada para todo lo que se ha hecho hoy.
También se adjunta el archivo MQL5.zip; tras desempaquetar este, podrá obtener directamente los archivos instalados para realizar las pruebas en las carpetas necesarias del terminal.
Programas utilizados en el artículo:
| # | Nombre | Tipo | Descripción |
|---|---|---|---|
| 1 | Table.mqh | Biblioteca de clases | Biblioteca de clases para crear tablas |
| 2 | Controls.mqh | Biblioteca de clases | Biblioteca de clases para crear controles gráficos |
| 3 | FrameViewer.mqh | Biblioteca de clases | Biblioteca de clases para implementar la funcionalidad de optimización visual en el asesor experto |
| 4 | ExpertMAMA_Frames.mq5 | Asesor | Asesor para pruebas de optimización visual |
| 5 | MQL5.zip | Archivo | Archivo con los ficheros presentados anteriormente para desempaquetar en el directorio MQL5 del terminal cliente. |
| 6 | Old_article_files.zip | Archivo | Archivo de ficheros del artículo original en el que se basan todos los archivos de este artículo |
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/17457
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Utilizando redes neuronales en MetaTrader
Características del Wizard MQL5 que debe conocer (Parte 55): SAC con Prioritized Experience Replay (PER)
Particularidades del trabajo con números del tipo double en MQL4
Redes neuronales en el trading: Clusterización doble de series temporales (Final)
- 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
Tras la divulgación del opt-format, el uso de marcos seguía siendo apropiado sólo cuando se transferían datos que no estaban en el opt-file.
En el ejemplo de este artículo, la GUI propuesta podría utilizarse para visualizar un opt-file.
Tras la divulgación del formato opt, el uso de tramas seguía siendo apropiado sólo cuando se transmitían datos que no estaban en el archivo opt.
En el ejemplo de este artículo, la GUI propuesta podría utilizarse para visualizar un archivo opt.