Русский Español
preview
Visualização de estratégias em MQL5: distribuindo os resultados da otimização em gráficos de critérios

Visualização de estratégias em MQL5: distribuindo os resultados da otimização em gráficos de critérios

MetaTrader 5Exemplos |
135 2
Artyom Trishkin
Artyom Trishkin

Conteúdo


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:

  1. Aba Optimization:

    1. tabela de resultados da otimização do passe atual,
    2. tabela de parâmetros de entrada do EA para esse passe,
    3. gráfico de saldo do passe de otimização concluído,
    4. botão Replay para reproduzir novamente a otimização realizada.

  2. Aba Sharpe Ratio:

    1. tabela de resultados da otimização do passe selecionado (um dos três melhores pelo coeficiente de Sharpe), 
    2. tabela de parâmetros de entrada do EA para o passe selecionado (um dos três melhores pelo coeficiente de Sharpe), 
    3. gráficos de saldo dos três melhores passes da otimização pelo coeficiente de Sharpe,
    4. 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.

  3. Aba Net Profit:

    1. tabela de resultados da otimização do passe selecionado (um dos três melhores por Lucro Total),
    2. tabela de parâmetros de entrada do EA para o passe selecionado (um dos três melhores por Lucro Total), 
    3. gráficos de saldo dos três melhores passes da otimização por Lucro Total,
    4. botão de seleção (com três posições) para escolher um dos três melhores resultados da otimização por Lucro Total.

  4. Aba Profit Factor:

    1. tabela de resultados da otimização do passe selecionado (um dos três melhores por Fator de Lucro), 
    2. tabela de parâmetros de entrada do EA para o passe selecionado (um dos três melhores por Fator de Lucro),  
    3. gráficos de saldo dos três melhores passes da otimização por Fator de Lucro, 
    4. 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.

  5. Aba Recovery Factor:

    1. tabela de resultados da otimização do passe selecionado (um dos três melhores por Fator de Recuperação),  
    2. tabela de parâmetros de entrada do EA para o passe selecionado (um dos três melhores por Fator de Recuperação),  
    3. gráficos de saldo dos três melhores passes da otimização por Fator de Recuperação,  
    4. 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 &params[], 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 &params[], 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

Arquivos anexados |
Table.mqh (66.62 KB)
Controls.mqh (183.33 KB)
FrameViewer.mqh (187.03 KB)
MQL5.zip (52 KB)
Últimos Comentários | Ir para discussão (2)
fxsaber
fxsaber | 21 mar. 2025 em 10:32

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.

Artyom Trishkin
Artyom Trishkin | 21 mar. 2025 em 10:53
fxsaber #:

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.

Não fui tão longe. Interessante, obrigado.
Otimização por neuroboides — Neuroboids Optimization Algorithm (NOA) Otimização por neuroboides — Neuroboids Optimization Algorithm (NOA)
Trata-se de uma nova metaheurística de otimização bioinspirada e autoral, denominada NOA (Neuroboids Optimization Algorithm), que combina princípios de inteligência coletiva e redes neurais. Ao contrário dos métodos clássicos, o algoritmo utiliza uma população de "neuroboides" autoaprendizes, cada um com sua própria rede neural, que adapta a estratégia de busca em tempo real. O artigo em questão apresenta a arquitetura do algoritmo, os mecanismos de autoaprendizado dos agentes e as perspectivas de aplicação dessa abordagem híbrida em tarefas complexas de otimização.
Redes neurais em trading: Dupla clusterização de séries temporais (Conclusão) Redes neurais em trading: Dupla clusterização de séries temporais (Conclusão)
Damos continuidade à implementação dos métodos propostos pelos autores do framework DUET, que apresenta uma abordagem inovadora para a análise de séries temporais, combinando clusterização temporal e de canais para revelar padrões ocultos nos dados analisados.
Arbitragem no Forex: Um bot market maker simples de sintéticos para começar Arbitragem no Forex: Um bot market maker simples de sintéticos para começar
Hoje vamos analisar meu primeiro robô na área de arbitragem, que é um provedor de liquidez (se é que podemos chamá-lo assim) em ativos sintéticos. Atualmente, esse bot funciona com sucesso como um módulo dentro de um grande sistema baseado em aprendizado de máquina, mas eu resgatei o antigo robô de arbitragem no Forex da nuvem, então vamos olhar para ele e pensar no que podemos fazer com ele hoje.
Simulação de mercado: Position View (XIII) Simulação de mercado: Position View (XIII)
Neste artigo, mostrarei como você, pode sem muito esforço, conseguir implementar a indicação se uma posição, está lhe dando prejuízo ou mesmo lucro. Isto de maneira extremamente simples e eficaz. Usando este indicador que estou mostrando como desenvolver, você, mesmo sem muito conhecimento, conseguirá facilmente saber quando é hora de fechar uma posição. E ao fazê-lo, não virá a ter um resultado diferente do esperado. Isto por que, estamos efetuando o calculo de forma a termos a real situação de nossa posição.