Visualização de estratégias em MQL5: distribuindo os resultados da otimização em gráficos de critérios
Conteúdo
- Introdução
- Como funciona
- Criando classes para nossas tarefas
- Elemento de controle Tab Control
- Classe de tabelas
- Classe de barra de progresso
- Classe de gráfico de estatísticas
- Classe de visualizador de frames
- Conectando o funcional ao EA
- Considerações finais
Introdução
O site mql5.com contém tanta informação que, toda vez que se navega novamente pelos catálogos de artigos, pela seção de referência ou pelo guia de aprendizado, inevitavelmente se descobre algo novo e interessante.
Foi assim também desta vez: encontrei um artigo simples e, à primeira vista, nada extraordinário, que descrevia brevemente o testador de estratégias. Tudo parecia básico e já conhecido, mas... a última parte do artigo chamou atenção. Ali, propunha-se simplesmente conectar um pequeno trecho de código ao EA, adicionar alguns manipuladores padrão, e... o otimizador comum do testador de estratégias da plataforma MetaTrader 5 se transforma em um otimizador visual. Hmm... interessante.
Comecei a estudar e entender o funcionamento. Como resultado, surgiu a ideia de melhorar um pouco o visual e ampliar as possibilidades de visualização dos resultados da otimização.
Faremos assim: o EA abrirá uma nova janela, onde estarão dispostas cinco abas. Na primeira haverá um gráfico de todos os passes, em que cada novo passe será mostrado como uma linha de saldo. Nas outras quatro abas também haverá gráficos, mas eles só estarão disponíveis ao final da otimização. Cada uma dessas abas exibirá dados sobre os três melhores passes com base em um dos quatro critérios de otimização. E em cada aba estarão presentes duas tabelas: uma com os resultados do passe da otimização e outra com as configurações do EA para aquele passe:
- Aba Optimization:
- tabela de resultados da otimização do passe atual,
- tabela de parâmetros de entrada do EA para esse passe,
- gráfico de saldo do passe de otimização concluído,
- botão Replay para reproduzir novamente a otimização realizada.
- Aba Sharpe Ratio:
- tabela de resultados da otimização do passe selecionado (um dos três melhores pelo coeficiente de Sharpe),
- tabela de parâmetros de entrada do EA para o passe selecionado (um dos três melhores pelo coeficiente de Sharpe),
- gráficos de saldo dos três melhores passes da otimização pelo coeficiente de Sharpe,
- botão de seleção (com três posições) para escolher um dos três melhores resultados de otimização pelo coeficiente de Sharpe.
- Aba Net Profit:
- tabela de resultados da otimização do passe selecionado (um dos três melhores por Lucro Total),
- tabela de parâmetros de entrada do EA para o passe selecionado (um dos três melhores por Lucro Total),
- gráficos de saldo dos três melhores passes da otimização por Lucro Total,
- botão de seleção (com três posições) para escolher um dos três melhores resultados da otimização por Lucro Total.
- Aba Profit Factor:
- tabela de resultados da otimização do passe selecionado (um dos três melhores por Fator de Lucro),
- tabela de parâmetros de entrada do EA para o passe selecionado (um dos três melhores por Fator de Lucro),
- gráficos de saldo dos três melhores passes da otimização por Fator de Lucro,
- botão de seleção (com três posições) para escolher um dos três melhores resultados da otimização por Fator de Lucro.
- Aba Recovery Factor:
- tabela de resultados da otimização do passe selecionado (um dos três melhores por Fator de Recuperação),
- tabela de parâmetros de entrada do EA para o passe selecionado (um dos três melhores por Fator de Recuperação),
- gráficos de saldo dos três melhores passes da otimização por Fator de Recuperação,
- botão de seleção (com três posições) para escolher um dos três melhores resultados da otimização por Fator de Recuperação.
Para implementar o conjunto de abas, criaremos classes de elementos de controle, a partir das quais formaremos o elemento de controle Tab Control. O processo de criação desses controles será omitido neste artigo; em vez disso, será fornecido um arquivo de classes pronto. Em artigos futuros, retornaremos à descrição detalhada dessas classes para a criação de alguns elementos de controle que poderão ser úteis posteriormente.
Para exibir informações sobre os parâmetros dos passes, precisaremos de classes de tabelas, que serão aproveitadas diretamente do artigo "Recursos do SQLite em MQL5: Exemplo de painel interativo com estatísticas de negociação por símbolos e mágicos". Faremos pequenas melhorias nessas classes de tabelas para facilitar a criação de novas tabelas e a exibição de texto em suas células.
Para realizar essa ideia, utilizaremos os códigos de trabalho com frames de otimização apresentados no artigo mencionado acima e, com base neles, criaremos nossas próprias classes, procurando preservar ao máximo o conceito original. Como o artigo não descreve detalhadamente o processo de trabalho com os frames e com o EA operando no modo frame, tentaremos aqui entender o funcionamento desse sistema.
Como funciona
Consultemos o Guia de MQL5 para ver o que está indicado sobre o funcionamento do testador de estratégias e seu otimizador:
... Uma função especialmente importante do testador é a otimização multiprocessada, que pode ser executada utilizando agentes locais e distribuídos (em rede), inclusive na MQL5 Cloud Network. Cada execução individual de teste (com parâmetros de entrada específicos do EA), iniciada manualmente pelo usuário, ou uma das muitas execuções geradas durante a otimização (quando há iteração dos valores dos parâmetros dentro dos intervalos definidos), é realizada em um programa separado chamado de agente. Tecnicamente, trata-se do arquivo metatester64.exe, e suas cópias podem ser vistas no Gerenciador de Tarefas do Windows durante o teste e a otimização. É exatamente por isso que o testador é multiprocessado.
O terminal atua como um gerenciador que distribui tarefas entre os agentes locais e remotos. Os agentes locais são iniciados automaticamente pelo terminal quando necessário. Durante a otimização, vários agentes são executados, sendo o número deles equivalente à quantidade de núcleos do processador. Após concluir cada tarefa de teste do EA com os parâmetros definidos, o agente devolve os resultados ao terminal.
Em cada agente é criado seu próprio ambiente de negociação e de execução de código. Todos os agentes são isolados uns dos outros e também do terminal do cliente.
Com base nessa descrição, é possível compreender que cada instância do EA em teste é executada em seu próprio agente de teste e que cada passe, ou seja, seus dados finais, é enviado do agente de volta ao terminal.
Para a troca de dados entre o terminal e os agentes, existe um conjunto de manipuladores (event handlers):
- OnTesterInit() — é chamado nos EAs quando ocorre o evento TesterInit, para executar as ações necessárias antes do início da otimização no testador de estratégias.
- OnTester() — é chamado nos EAs quando ocorre o evento Tester, para executar as ações necessárias ao final de um teste.
- OnTesterPass() — é chamado nos EAs quando ocorre o evento TesterPass, para processar um novo frame de dados durante a otimização do EA.
- OnTesterDeinit() — é chamado nos EAs quando ocorre o evento TesterDeinit, para executar as ações necessárias ao final da otimização do EA.
Se o EA contiver um dos manipuladores OnTesterInit() ou OnTesterDeinit() (que sempre funcionam em conjunto, não sendo possível ter apenas um deles) ou OnTesterPass(), ele será executado em uma janela separada do terminal, em um modo especial chamado frame-mode.
Para controlar o andamento da otimização e transmitir do agente para o terminal resultados personalizados (além dos indicadores de desempenho da negociação), o MQL5 possui três eventos especiais: OnTesterInit, OnTesterDeinit e OnTesterPass. Ao definir manipuladores para esses eventos no código, o programador obtém a capacidade de executar as ações necessárias antes do início da otimização, após sua conclusão e ao término de cada passe individual de otimização.
Todos os manipuladores são opcionais. A otimização funciona mesmo sem eles. Também é importante compreender que esses três eventos operam apenas durante o processo de otimização, e não durante um teste único.
O EA que contém esses manipuladores é carregado automaticamente em um gráfico separado do terminal, com o símbolo e o período especificados no testador. Essa cópia do EA não realiza operações de negociação, mas executa exclusivamente funções de serviço. Nela, todos os outros manipuladores de eventos não são executados, em particular OnInit, OnDeinit e OnTick.
Durante a otimização, apenas uma instância do EA opera no terminal e, quando necessário, recebe os frames que chegam. Mas é importante reforçar que tal instância do EA é iniciada somente se em seu código existir pelo menos um dos três manipuladores de eventos mencionados.
Após a conclusão de cada passe individual do otimizador, na instância do EA que está sendo executada no agente é gerado o evento OnTester(). A partir do manipulador desse evento, é possível enviar os dados do passe para o EA que está sendo executado no gráfico separado, em modo especial de frame. O pacote de dados referentes ao passe concluído, que é enviado para o EA no gráfico, é chamado de frame, e ele contém informações como o número do passe, os valores das variáveis de entrada do EA com as quais aquele passe foi executado, e os resultados obtidos nesse passe.
Todos esses dados são recebidos pelo EA, que então gera o evento TesterPass, processado no manipulador OnTesterPass(), onde podemos ler os dados do passe e realizar determinadas ações (neste caso, por exemplo, desenhar o gráfico de saldo desse passe e executar outras funções de serviço).
Para enviar os dados de um passe do agente para o EA no gráfico do terminal, é necessário usar a função FrameAdd(). O frame atual (ou seja, o passe concluído) será transmitido do agente para o EA e, então, processado no manipulador OnTesterPass().
Como podemos ver, algumas funções são executadas no agente, dentro da instância do EA em funcionamento ali, enquanto outras operam no EA que está no gráfico do terminal, funcionando em modo de frame. No entanto, todas elas, naturalmente, precisam estar descritas dentro do código do próprio EA.
Assim, a sequência de operação do EA e de nossas ações ao transmitir dados entre o agente e o terminal é a seguinte:
- No manipulador OnTesterInit (instância do EA no gráfico do terminal), é necessário preparar toda a parte gráfica: o gráfico separado em que o EA é executado no modo frame, o gráfico de saldo, as tabelas com parâmetros e resultados, o elemento de controle de abas (Tab Control) e os botões de seleção de ações em cada aba.
- No manipulador OnTester (instância do EA no agente), é necessário reunir todas as informações sobre o passe concluído: registrar no array o resultado do saldo de cada operação de fechamento; obter e armazenar em um array os resultados gerais desse passe; e, em seguida, enviar todos esses dados ao EA usando a função FrameAdd().
- No manipulador OnTesterPass (instância do EA no gráfico do terminal), é recebido o próximo frame enviado do agente através de FrameAdd(); seus dados são lidos e o gráfico de saldo é desenhado no chart, um objeto-frame é criado e armazenado em um array para posterior ordenação e seleção segundo os critérios de otimização.
- Nos manipuladores OnTesterDeinit e OnChartEvent (instância do EA no gráfico do terminal), é realizado o trabalho com os dados da otimização após sua conclusão, como a reprodução do processo de otimização e a exibição dos melhores resultados segundo determinados critérios de otimização.
Criando classes para nossas tarefas
Para a criação do elemento de controle de abas (Tab Control), foi desenvolvido o arquivo Controls.mqh, contendo um conjunto de elementos de controle. Ele está anexado ao final do artigo e deve ser colocado diretamente na pasta em que o EA de teste será criado, por exemplo, no diretório do terminal MQL5\Experts\FrameViewer.
Aqui não detalharemos cada classe criada para cada elemento de controle. mas apenas faremos uma breve visão geral.
Ao todo, foram criadas dez classes para oito elementos de controle independentes:
| # | Classe | Classe pai | Descrição | Propósito |
|---|---|---|---|---|
| 1 | CBaseCanvas | CObject | Classe base de desenho | Tela base. Contém métodos para definir e alterar dimensões e posição, ocultar e exibir elementos |
| 2 | CPanel | CBaseCanvas | Classe de painel | Contém métodos de definição e alteração de cores e manipuladores de eventos do mouse. Permite anexar elementos de controle filhos |
| 3 | CLabel | CPanel | Classe de rótulo de texto | Exibe texto sobre a tela nas coordenadas especificadas |
| 4 | CButton | CLabel | Classe de botão simples | Botão comum com estado não fixo. Reage à aproximação do cursor e aos cliques do mouse alterando a cor |
| 5 | CButtonTriggered | CButton | Classe de botão de duas posições | Botão com dois estados, isto é, Ligado/Desligado. Reage à aproximação do cursor, cliques do mouse e troca de estado mudando a cor |
| 6 | CTabButton | CButtonTriggered | Classe de botão de aba | Botão de duas posições sem borda na área de conexão com o campo da aba |
| 7 | CButtonSwitch | CPanel | Classe de botão seletor | Painel contendo dois ou mais botões de duas posições, em que apenas um pode estar no estado Ligado. Permite adicionar novos botões aos já existentes |
| 8 | CTabWorkArea | CObject | Classe da área de trabalho da aba | Objeto que contém duas classes base de desenho — nomeadamente uma para o plano de fundo e outra para o primeiro plano |
| 9 | CTab | CPanel | Classe do objeto de aba | Painel que contém um botão e um campo. No campo da aba está a área de trabalho, onde ocorre o desenho dos elementos |
| 10 | CTabControl | CPanel | Classe do objeto de controle de abas | Painel que permite adicionar objetos de aba à sua composição e gerenciá-los |
Após a criação bem-sucedida do objeto de controle, para cada um dos objetos deve ser chamado seu método Create(), especificando suas coordenadas e dimensões. Depois disso, o elemento estará pronto para ser utilizado.
Um elemento de controle que contenha manipuladores de eventos implementados em sua estrutura envia ao gráfico do programa principal eventos personalizados, através dos quais é possível identificar o que foi feito dentro do objeto:
| # | Classe | Evento | Identificador | lparam | dparam | sparam |
|---|---|---|---|---|---|---|
| 1 | CButton | Clique no objeto | (ushort)CHARTEVENT_CLICK | coordenada X do cursor | coordenada Y do cursor | nome do objeto do botão |
| 2 | CButtonTriggered | Clique no objeto | (ushort)CHARTEVENT_CLICK | coordenada X do cursor | coordenada Y do cursor | nome do objeto do botão |
| 3 | CTabButton | Clique no objeto | (ushort)CHARTEVENT_CLICK | coordenada X do cursor | coordenada Y do cursor | nome do objeto do botão |
| 4 | CButtonSwitch | Clique em um botão do objeto | (ushort)CHARTEVENT_CLICK | identificador do botão | 0 | nome do objeto do botão seletor |
A partir da tabela, podemos observar que, para simplificar o código, o elemento de controle Tab Control não envia eventos personalizados diretamente para o gráfico do programa. Se o programa precisar reagir à troca de abas, é possível detectar o evento pelo clique no botão da aba (TabButton). Pelo nome do botão, pode-se determinar o número da aba, ou consultar o índice da aba selecionada diretamente a partir do objeto TabControl, entre outras possibilidades.
De qualquer forma, posteriormente analisaremos detalhadamente classes desse tipo ao desenvolver diversos elementos de controle úteis que poderão ser utilizados em nossos próprios programas.
Agora precisamos aprimorar um pouco a classe de tabelas, apresentada no artigo anterior, que deve ser baixada (arquivo Dashboard.mqh). Deve-se copiar do arquivo apenas o código da classe de tabelas (linhas 12 a 285) e salvar o código copiado na pasta \MQL5\Experts\FrameViewer\ com o nome Table.mqh.
Faremos modificações na classe para tornar o trabalho com tabelas e dados tabulares um pouco mais prático.
Conectaremos ao arquivo o módulo da Classe de array dinâmico de ponteiros para instâncias da classe CObject e seus herdeiros CArrayObj, e o arquivo da classe CCanvas, que simplifica a criação de elementos gráficos personalizados:
//+------------------------------------------------------------------+ //| 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>
Na seção privada da classe da célula da tabela, adicionaremos novas variáveis para armazenar a largura, a altura e a cor do texto dentro da célula:
//+------------------------------------------------------------------+ //| Класс ячейки таблицы | //+------------------------------------------------------------------+ 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:
Na seção pública, incluiremos métodos para leitura e definição dessas novas propriedades, além de um método que exibirá o texto armazenado na célula em um 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); } //--- Виртуальный метод сравнения двух объектов
No final do código da classe, escreveremos uma nova classe de controle de tabelas:
//+------------------------------------------------------------------+ //| Класс управления таблицами | //+------------------------------------------------------------------+ 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(); } //+------------------------------------------------------------------+
Nessa classe estarão os métodos cujo princípio foi descrito no artigo "Criando um painel informativo para exibição de dados em indicadores e EAs" na parte que tratava da descrição do painel informativo. No artigo mencionado, esses métodos pertenciam ao objeto painel. Aqui, eles foram separados em uma classe independente, herdada da classe de tabela.
Todos os objetos de dados tabulares terão o tipo da classe CTableDataControl — o objeto controlador de tabelas, que permitirá gerenciar as tabelas de forma prática e eficiente.
Vejamos o que, naquele artigo mais antigo, era proposto carregar e conectar ao EA:
E o último "detalhe essencial" do nosso programa é o trabalho com os resultados da otimização! Antes, o trader precisava preparar os dados, exportá-los e processá-los separadamente em outro ambiente. Agora, porém, isso pode ser feito diretamente no próprio testador, durante o processo de otimização. Para demonstrar essa possibilidade, são necessários alguns arquivos incluídos, que contêm exemplos simples de como realizar esse tipo de processamento.
Copiamos os arquivos anexados ao artigo, com extensão MQH, para a pasta MQL5\Include. Escolhemos qualquer EA e adicionamos, ao final do código, o seguinte bloco:
//--- подключим код для работы с результатами оптимизации #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 между кадрами } //+------------------------------------------------------------------+
Como exemplo, utilizamos o EA padrão que acompanha o MetaTrader, o Moving Averages.mq5. Inserimos o código, salvamos o EA com o nome Moving Averages With Frames.mq5. Compilamos e iniciamos a otimização.
No final do artigo, observamos os arquivos anexados. Há quatro arquivos com a extensão *.mqh. Copiamos esses arquivos para o nosso diretório e analisamos suas funções:
- specialchart.mqh (7.61 KB) — classe de gráfico especial, na qual são desenhadas as linhas de saldo de cada passe do testador e as linhas de saldo ao reproduzir o processo de uma otimização concluída;
- colorprogressbar.mqh (4.86 KB) — classe de barra de progresso, que exibe o andamento da otimização preenchendo-se com colunas coloridas durante o processo. A cor verde indica uma sequência lucrativa e a vermelha indica uma sequência de prejuízo; a barra é exibida na parte inferior do gráfico especial;
- simpletable.mqh (10.74 KB) — classe de tabela simples, na qual são mostrados os dados de cada passe da otimização, o resultado obtido e os valores dos parâmetros de configuração do EA utilizados naquele passe. Duas tabelas são exibidas à esquerda dos gráficos do gráfico especial;
- framegenerator.mqh (14.88 KB) — classe para troca de dados entre o agente de teste e o terminal, além da exibição das informações no gráfico especial. É a principal classe responsável pela implementação da otimização visual.
Com base no conhecimento adquirido, decidimos criar (1) uma classe de barra de progresso, (2) uma classe de gráfico especial e (3) uma classe de visualizador de frames. A classe de tabelas (4) nós já temos, ela foi carregada para a pasta do futuro EA e recebeu algumas melhorias.
Precisaremos também criar uma pequena classe, a classe de frame (5). Para quê? Exibiremos e selecionaremos os gráficos dos três melhores passes para cada um dos quatro critérios de otimização: Índice de Sharpe, Lucro Total, Fator de Lucro e Fator de Recuperação. Isso será mais prático se tivermos uma lista de objetos criada com base em uma classe de array dinâmico de ponteiros para instâncias da classe CObject e seus herdeiros da biblioteca padrão. Basta ordenar essa lista conforme o critério desejado para que todos os objetos nela sejam automaticamente ordenados de acordo com o valor da propriedade correspondente ao critério escolhido. O objeto com o valor máximo do parâmetro ficará no final da lista. Então, basta encontrar mais dois objetos com valores imediatamente inferiores ao do anterior. Os métodos necessários para essa busca já estão implementados na classe mencionada.
As classes da barra de progresso, do gráfico especial e do visualizador de frames serão criadas com base nos códigos obtidos a partir do artigo anterior. Apenas observaremos como elas foram implementadas e, a partir dessa base, criaremos nossas versões, ajustando e removendo o que for desnecessário, além de acrescentar o que for útil. Analisaremos o código resultante, que poderá ser comparado aos originais da antiga publicação — o arquivo com os códigos antigos será anexado ao final deste artigo.
Todos os códigos das classes serão escritos em um único arquivo. Criaremos esse arquivo (caso ainda não exista) na pasta \MQL5\Experts\FrameViewer\FrameViewer.mqh e começaremos a preenchê-lo.
Conectaremos ao arquivo criado os arquivos das classes e bibliotecas necessárias, além de definir algumas macros:
//+------------------------------------------------------------------+ //| 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 // Идентификатор таблицы входных параметров на вкладке оптимизации
Quase todos os objetos gráficos que realizarão desenhos possuem vários objetos do tipo CCanvas. Um pode servir como base, sobre a qual são dispostos dois outros: o primeiro é usado para desenhar o fundo e o segundo para desenhar o que deve aparecer sobre esse fundo. Para os objetos cujos métodos são responsáveis pelo desenho, os ponteiros para o canvas correspondente são passados como parâmetro para os métodos que executarão a renderização.
Como há uma quantidade considerável de código nas classes, e cada classe e seus métodos já estão detalhadamente comentados, não descreveremos tudo de forma minuciosa e passo a passo. Apenas analisaremos o código das classes e métodos, fazendo uma breve revisão baseada no material apresentado.
Então, a classe da barra de progresso:
//+------------------------------------------------------------------+ //| Класс прогресс-бара, рисующий двумя цветами | //+------------------------------------------------------------------+ 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); }
Nesta classe, não há objetos próprios de canvas para desenho. Para indicar o canvas sobre o qual o desenho será feito, existe um método que recebe como parâmetro o ponteiro para um canvas existente, o qual é atribuído às variáveis da classe. É nesse canvas que os métodos da classe realizarão o desenho. Há dois objetos envolvidos: um para desenhar o fundo da barra de progresso e outro para desenhar o conteúdo no primeiro plano, sobre o fundo já renderizado. O canvas será composto por objetos CCanvas da classe de gráfico especial, no qual a barra de progresso será exibida.
Classe para desenhar gráficos de estatísticas e tabelas com os resultados da otimização e os parâmetros de configuração do EA:
//+------------------------------------------------------------------+ //| Класс для отрисовки графиков статистики и таблиц | //| результатов оптимизации и параметров настройки советника | //+------------------------------------------------------------------+ 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); } };
A classe desenha no canvas especificado (fundo e primeiro plano) as tabelas de parâmetros e resultados dos testes, os gráficos dos passes, a barra de progresso e os botões para iniciar a reprodução do processo de otimização concluído e selecionar os melhores resultados conforme determinados critérios de otimização.
Vale destacar que, para indicar os limites da área retangular do canvas dentro da qual um objeto ou região deve ser rastreado, as classes aqui abordadas utilizam a estrutura CRect.
Essa estrutura está descrita no arquivo \MQL5\Include\Controls\Rect.mqh e serve como uma ferramenta prática para definir os limites de uma área retangular que contenha elementos importantes. Por exemplo, é possível limitar, dentro do canvas, a área onde o movimento do cursor do mouse deve ser monitorado, ou definir o tamanho de um retângulo que abranja toda a extensão do canvas. Nesse caso, toda a área do objeto estará disponível para interação com o cursor. A estrutura possui métodos que retornam as coordenadas dos limites da área retangular. É possível definir e obter esses valores de várias formas, dependendo das necessidades e da estrutura dos objetos. Também há métodos para mover ou deslocar a área retangular. Em resumo, trata-se de uma ferramenta conveniente para definir e acompanhar regiões específicas dentro de um canvas.
Nas classes aqui discutidas, essas áreas são necessárias para permitir a interação com o cursor do mouse e indicar onde os objetos estão posicionados no canvas.
Método para atualização do 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); }
A área retangular do canvas destinada ao desenho dos gráficos dos passes é limpa; em seguida, nela é desenhada a linha de saldo e a barra de progresso.
Método que adiciona uma nova série de dados para desenho no 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; }
A cada novo passe do otimizador, ou seja, a cada novo conjunto de dados, um novo registro deve ser feito em um array de séries, e é exatamente isso que este método faz.
Métodos para obter os valores máximo e mínimo de uma série específica no array de passes do otimizador:
//+------------------------------------------------------------------+ //| Получение максимального значения указанной серии | //+------------------------------------------------------------------+ 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 posicionar os gráficos dos passes do otimizador de forma centralizada no gráfico especial, é necessário conhecer os valores máximo e mínimo da série correspondente ao passe. Com base nesses valores, é possível calcular as coordenadas relativas da linha no gráfico, de modo que ela se ajuste a 80% do espaço total destinado ao desenho das curvas de saldo dos passes do otimizador.
Método para desenhar a linha de saldo no 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); }
Esse é um método sobrecarregado com o mesmo nome do método da classe CCanvas. No gráfico, as coordenadas começam no canto superior esquerdo. Enquanto, nas curvas de saldo tradicionais, as coordenadas têm origem no canto inferior esquerdo.
Nesse método, as coordenadas Y da tela são invertidas, para que a linha de saldo seja desenhada corretamente, seguindo os valores das posições de saldo armazenados no array.
Método que desenha as linhas de saldo no 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); }
Aqui são calculadas as coordenadas necessárias da linha de saldo dentro da área do gráfico destinada ao desenho dos saldos dos passes. Em seguida, dentro de um laço sobre o array da série especificada, são traçadas as linhas que conectam todos os pontos de saldo registrados no array.
Classe de dados do 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) {} };
Após a conclusão de cada passe do otimizador, um frame é enviado ao terminal. Nesse frame estão contidos todos os dados obtidos ao final daquele passe. Para acessar as informações de um passe específico, seria necessário percorrer todos os frames recebidos em busca daquele com o número desejado e então extrair seus dados. Isso, evidentemente, não é eficiente. Precisamos de um método que permita o acesso rápido aos dados do passe necessário, além da possibilidade de ordenar todos os passes de acordo com uma determinada propriedade, afinal, precisaremos selecionar os três melhores passes com base em um dos quatro critérios de otimização.
A solução é armazenar (cachear) os passes. Para isso, criamos uma classe de objeto frame. Após a conclusão de cada passe e o envio do frame ao terminal, será criado um objeto frame, cujas propriedades serão preenchidas com os dados do frame recebido do testador. Esse objeto será então adicionado a uma lista. Posteriormente, quando o processo de otimização for concluído e todos os frames tiverem sido recebidos, nossa lista conterá cópias de todos os frames. Assim, poderemos ordenar essa lista com base em qualquer uma das propriedades desejadas e acessar rapidamente os dados do frame correspondente.
Vale observar que, no método Compare(), foi necessário realizar a comparação dos números de ponto flutuante não através da diferença normalizada em relação a zero, mas comparando dois números normalizados entre si. Por quê?
Para comparar dois números de ponto flutuante, existem diferentes abordagens possíveis. A primeira é comparar os números sem normalizá-los. Primeiro, verificar se um é maior que o outro; depois, se um é menor que o outro; e, por fim, considerar "igual" o que restar. Outra forma é comparar a diferença normalizada entre os dois números em relação a zero. Nesse caso, porém, foi necessário normalizar ambos os números para duas casas decimais e, em seguida, comparar esses valores arredondados.
O motivo é que, no terminal, na tabela de resultados da otimização, os valores exibidos são apresentados com duas casas decimais. Internamente, porém, esses números não estão realmente normalizados. Ou seja, o formato com duas casas é apenas uma representação visual na tabela. Assim, se a tabela mostra, por exemplo, 1.09 e 1.08, os valores reais podem ser algo como 1.085686399864 e 1.081254322375. Ambos são exibidos como 1.09 e 1.08. Mas, ao comparar sem normalização, pode ocorrer que ambos os valores acabem arredondados para a mesma magnitude. E se a normalização não for aplicada, o valor 1.09 pode nem sequer existir em sua forma interna. Isso levaria a erros na busca pelos melhores passes.
A solução é normalizar ambos os números para duas casas decimais e, só então, comparar os valores já arredondados.
Classe do visualizador 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(); } };
Sabemos exatamente quantas abas haverá e quais elementos estarão dispostos em cada uma. Por isso, não é necessário criar novos objetos dinamicamente, sendo apenas declaradas as instâncias dos objetos necessários para cada aba, além dos métodos de acesso a eles e dos métodos de operação da classe.
No construtor:
//+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ 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(); }
No construtor, são obtidos e armazenados a largura e a altura do gráfico em que o EA está sendo executado; em seguida, é encontrado e registrado o ponteiro para a barra de progresso, e a lista de frames é limpa.
Antes de iniciar a otimização, é necessário preparar o gráfico no qual a cópia do EA em modo frame será executada no terminal cliente. Esse gráfico será destacado do terminal e, sobre ele, ocupando toda a sua área, será colocado o elemento de controle Tab Control, a em suas abas, estarão posicionados os demais elementos nos quais serão exibidos os gráficos de saldo dos passes e os botões de controle.
Tudo isso deve ser feito no manipulador OnTesterInit(). Para isso, a classe possui manipuladores com o mesmo nome, que são chamados pelo EA a partir da instância da classe CFrameViewer.
Manipulador 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; }
Aqui, a criação de todos os elementos é feita em blocos. Cada bloco de código é responsável pela criação de um elemento específico da interface do programa.
Após a otimização, é preciso fazer algumas modificações na interface criada, como alterar as cores dos títulos dos gráficos, modificar os textos e exibir o botão para iniciar a reprodução do processo de otimização na primeira aba (com identificador 0). Tudo deve ser executado no manipulador OnTesterDeinit().
Manipulador 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(); }
Após a conclusão de cada passe do otimizador, é gerado o evento Tester, que pode ser processado no manipulador OnTester(). Ele é executado no lado do agente de teste, na instância do EA que está rodando naquele agente.
Nesse manipulador, é necessário reunir todas as informações sobre o passe concluído, formar um frame e enviá-lo ao terminal cliente utilizando a função FrameAdd().
Manipulador 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()); }
Quando o EA no terminal cliente recebe um frame enviado pelo agente, é gerado o evento TesterPass, o qual é processado no manipulador OnTesterPass().
Nesse manipulador, as informações do frame são lidas, o gráfico de saldo do passe correspondente é desenhado no chart e as tabelas de resultados e parâmetros de teste são preenchidas. O frame processado é então salvo em um novo objeto frame, que é armazenado em uma lista de frames, permitindo que ele seja facilmente acessado quando for necessário localizar passes específicos para exibição nos gráficos.
Manipulador 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(); } } }
Após o término do processo de otimização, o EA que foi executado no modo frame permanecerá ativo no gráfico flutuante do terminal. Todo o trabalho com esse EA será organizado dentro do manipulador OnChartEvent(), pois o controle dos processos será feito por meio das interações do usuário com as abas e os botões exibidos no gráfico, além do uso do cursor do mouse.
Manipulador 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); } } }
Os eventos de troca de abas do elemento Tab Control e de clique nos botões do seletor são processados em seus respectivos manipuladores personalizados. As ações executadas neles são idênticas. Diferenciando-se apenas pelo identificador da aba. Por esse motivo, esses eventos possuem manipuladores separados para facilitar a organização do código.
Manipulador de troca de aba:
//+------------------------------------------------------------------+ //| Обработчик переключения вкладки | //+------------------------------------------------------------------+ 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); }
Manipulador de alternância do botão seletor:
//+------------------------------------------------------------------+ //| Обработчик переключения кнопки-переключателя | //+------------------------------------------------------------------+ 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 desenha as tabelas de dados e o gráfico de otimização:
//+------------------------------------------------------------------+ //| Рисует таблицы данных и график оптимизации | //+------------------------------------------------------------------+ 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); }
Após desenhar todas as tabelas e gráficos, é necessário posicionar corretamente os elementos de controle. Os botões das abas inativas devem ser ocultados, enquanto os da aba ativa devem ser exibidos, o que é feito pelo método ControlObjectsView.
Método que gerencia a exibição dos controles nos gráficos de otimização:
//+-------------------------------------------------------------------+ //|Управляет отображением управляющих объектов на графиках оптимизации| //+-------------------------------------------------------------------+ 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 a reprodução dos frames após o término da otimização:
//+------------------------------------------------------------------+ //| Повторное проигрывание фреймов после окончания оптимизации | //+------------------------------------------------------------------+ 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 os frames recebidos após a otimização estão disponíveis para visualização. Nesse caso, em um loop simples, percorremos todos os frames desde o primeiro até o último, exibindo seus dados nas tabelas e no gráfico.
Método que exibe os dados do frame especificado no gráfico de otimização:
//+------------------------------------------------------------------+ //| Выводит данные указанного фрейма на график оптимизации | //+------------------------------------------------------------------+ 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 os frames disponíveis após a otimização podem ser acessados apenas através da iteração sequencial usando FrameFirst() --> FrameNext(), e não por outros métodos diretos, este método percorre todos os frames até encontrar aquele cujo número de passe corresponde ao desejado. Assim que o frame necessário é localizado, seus dados são exibidos no gráfico.
Basicamente, após a otimização, já temos uma lista pronta de objetos frame, e podemos rapidamente obter o objeto desejado dessa lista. Esse tipo de acesso direto ao frame pode ser usado, mas, nesse caso, seria necessário escrever métodos adicionais para extrair os dados do objeto frame e do array de séries, convertê-los para o formato adequado e então exibi-los no gráfico. Por enquanto, o acesso foi mantido exatamente como está no método anterior, para reduzir a quantidade de código na classe e simplificar sua compreensão.
Método que desenha os gráficos dos três melhores passes segundo o critério de otimização:
//+------------------------------------------------------------------+ //| Рисует графики трёх лучших проходов по критерию оптимизации | //+------------------------------------------------------------------+ 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); }
Primeiro, é preenchido um array com os índices dos frames correspondentes aos três melhores passes, por meio do método FillArrayBestFrames(), e, em seguida, o passe necessário (ou todos os três) é exibido no gráfico.
Método que preenche o array com os índices dos frames dos três melhores passes para o critério de otimização 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 a lógica do método está detalhada nos comentários do código. Ao final de sua execução, o array, com tamanho igual a 3, conterá os números dos três melhores passes conforme o critério de otimização associado ao número da aba, na qual devem ser exibidos os dados desses passes. Para encontrar os frames cujas propriedades tenham valores menores que os do frame atual, é utilizado o método FrameSearchLess().
Método para buscar e retornar o ponteiro para o objeto frame com valor de propriedade menor que o de referência:
//+------------------------------------------------------------------+ //| Ищет и возвращает указатель на объект фрейма, | //| со значением свойства меньше образца | //+------------------------------------------------------------------+ 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; }
O método recebe um frame e, dentro da lista ordenada de frames, utiliza o método SearchLess() da classe CArrayObj da Biblioteca Padrão para localizar o objeto mais próximo cujo valor de propriedade seja inferior ao do frame passado como parâmetro.
Método que exibe nos gráficos de resultados da otimização, em cada aba, os três melhores passes:
//+------------------------------------------------------------------+ //| Выводит на графики результатов оптимизации | //| на каждой вкладке по три лучших прохода | //+------------------------------------------------------------------+ void CFrameViewer::DrawBestFrameDataAll(void) { //--- В цикле по всем вкладкам от вкладки 1, рисуем графики трёх лучших проходов для каждой вкладки for(int i=1; i<this.m_tab_control.TabsTotal(); i++) this.DrawBestFrameData(i,-1); }
Método para obter os dados do frame atual e exibi-los na aba correspondente, tanto na tabela quanto no gráfico de resultados da otimização:
//+------------------------------------------------------------------+ //| Получение данных текущего фрейма и вывод их на указанной вкладке | //| в таблицу и на график результатов оптимизации | //+------------------------------------------------------------------+ 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; }
Neste método, os dados são obtidos a partir do frame, as tabelas são preenchidas com essas informações e o gráfico de saldo do passe de otimização é desenhado.
Método que desenha a tabela de estatísticas da otimização na aba 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); }
Esse método desenha a tabela com os resultados da otimização, preenchendo apenas os títulos das linhas da tabela. As células com os dados são inseridas posteriormente, no método descrito acima.
Método que desenha a tabela de parâmetros de entrada da otimização na aba 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); }
Esse método, de forma semelhante ao anterior, desenha uma tabela vazia de parâmetros de otimização, que será preenchida com os dados no método DrawFrameData(), quando já forem conhecidos os parâmetros utilizados no passe do testador.
Método que desenha o gráfico de otimização na aba 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); }
O método prepara um gráfico limpo com o título apropriado, sobre o qual os métodos de desenho exibirão as linhas de saldo dos passes concluídos da otimização.
Concluímos completamente a criação de todas as classes necessárias para a otimização visual. Agora, o arquivo da classe CFrameViewer pode ser conectado a qualquer EA para visualizar o andamento de sua otimização em um gráfico separado dentro do terminal.
Conectando o funcional ao EA
Vamos verificar o resultado do que foi desenvolvido.
Pegaremos o EA da distribuição padrão, localizado em \MQL5\Experts\Advisors\ExpertMAMA.mq5 e o salvaremos na nova pasta já criada \MQL5\Experts\FrameViewer\ com o nome ExpertMAMA_Frames.mq5.
Tudo o que precisamos fazer é, ao final do código, incluir o arquivo da classe CFrameViewer, declarar um objeto do tipo dessa classe e adicionar os manipuladores de eventos nos quais serão chamadas as rotinas correspondentes do mesmo nome da classe criada.
O comprimento dos nomes das variáveis de entrada do EA pode ser ligeiramente reduzido, removendo os caracteres de sublinhado ("_") dos nomes. Isso proporcionará mais espaço para que os nomes caibam na largura das células da tabela.
//+------------------------------------------------------------------+ //| 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 между кадрами воспроизведения }
Essas são todas as alterações e complementos necessários no EA (além do encurtamento dos nomes das variáveis) para que a otimização visual funcione corretamente.
Vamos compilar o EA e executá-lo para otimização.
As configurações da otimização não são particularmente importantes, sendo necessário apenas definir parâmetros simples para testar o funcionamento do programa:

e iniciar o processo de otimização:

Antes do início da otimização, é aberto um novo gráfico no qual estão dispostos todos os elementos de controle. Isso é bastante conveniente, pois evita a necessidade de alternar entre os gráficos anexados com os resultados da otimização e o gráfico da otimização visual. Essa janela separada pode ser movida para fora da área do terminal ou para um segundo monitor, permitindo o acesso simultâneo a todos os gráficos de otimização.
Considerações finais
Em conclusão, podemos dizer que este foi apenas um exemplo simples de como é possível criar funcionalidades adicionais para monitorar o processo de otimização. No gráfico da otimização visual, é possível exibir qualquer dado obtido dos relatórios do testador ou mesmo cálculos personalizados feitos após cada passe da otimização. O tipo de funcionalidade e o estilo de visualização dependem do gosto e das necessidades de cada desenvolvedor que utiliza a otimização visual para obter os resultados desejados e maior praticidade na análise dos dados. Aqui, o ponto principal é que, com exemplos práticos, vimos como implementar e utilizar todos os recursos necessários de forma eficiente.
Todos os arquivos mencionados no artigo estão anexados para estudo detalhado. No arquivo compactado Old_article_files.zip encontram-se os arquivos originais do artigo anterior, sobre o qual todo o trabalho atual foi baseado.
Também foi anexado o arquivo compactado MQL5.zip, que, ao ser descompactado, já coloca automaticamente todos os arquivos necessários para o teste nas pastas corretas do terminal.
Programas utilizados no artigo:
| # | Nome | Tipo | Descrição |
|---|---|---|---|
| 1 | Table.mqh | Biblioteca de classes | Biblioteca de classes para criação de tabelas |
| 2 | Controls.mqh | Biblioteca de classes | Biblioteca de classes para criação de elementos gráficos de controle |
| 3 | FrameViewer.mqh | Biblioteca de classes | Biblioteca de classes para implementação, no EA, da funcionalidade de otimização visual |
| 4 | ExpertMAMA_Frames.mq5 | Expert Advisor | EA para teste da funcionalidade de otimização visual |
| 5 | MQL5.zip | Arquivo compactado | Arquivo com todos os arquivos mencionados acima, prontos para extração na pasta MQL5 do terminal cliente |
| 6 | Old_article_files.zip | Arquivo compactado | Arquivo contendo os materiais da publicação original, com base nos quais todos os arquivos deste artigo foram criados |
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/17457
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.
Otimização por neuroboides — Neuroboids Optimization Algorithm (NOA)
Redes neurais em trading: Dupla clusterização de séries temporais (Conclusão)
Arbitragem no Forex: Um bot market maker simples de sintéticos para começar
Simulação de mercado: Position View (XIII)
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso
Após a divulgação do formato opt, o uso de quadros permaneceu apropriado apenas para a transferência de dados que não estejam no arquivo opt.
No exemplo deste artigo, a GUI proposta poderia ser usada para visualizar um opt-file.
Após a divulgação do formato opt, o uso de quadros permaneceu apropriado somente ao transmitir dados que não estão no arquivo opt.
No exemplo deste artigo, a GUI proposta poderia ser usada para visualizar um arquivo opt.