Implementado OLAP na negociação (Parte 2): Visualizando resultados da análise interativa de dados multidimensionais

Stanislav Korotky | 21 junho, 2019

No primeiro artigo dedicado a OLAP na negociação, revisamos os princípios gerais do processamento analítico de dados multidimensionais e fornecemos classes MQL prontas que permitiam aplicar OLAP na prática para o histórico de contas ou para os relatórios negociação. Além disso, a exibição de resultados da análise foi implementada num formato de texto simplificado no log de EAs. Para melhor visibilidade, era preciso criar uma nova classe, um sucessor da interface Display, capaz de visualizar dados OLAP usando gráficos de negociação. Como se viu, essa tarefa requer muito trabalho preparatório e abrange muitos aspectos diferentes que não estão relacionados ao OLAP. Sendo assim, deixemos de lado o processamento de dados por enquanto e, gradualmente, passo a passo, trabalhemos a interface gráfica de programas MQL.

Para implementar uma interface gráfica em MQL, existem várias bibliotecas, incluindo a Biblioteca padrão de elementos de controle (Include/Controls). Uma das omissões importantes em quase todas as bibliotecas se deve ao fato de que não há meios de controlar automaticamente a disposição dos elementos na janela. Em outras palavras, o posicionamento e o alinhamento dos elementos são realizados estaticamente usando constantes hard-coded no código com as coordenadas X e Y. Com este problema está intimamente relacionada a falta de meios de design visual de formulários de tela. Esta é uma tarefa ainda mais difícil, mas não impossível. Como neste projeto a interface ainda não era o tópico principal, foi decidido não prestar atenção ao editor de formulários de tela e focar numa abordagem mais simples com a implementação de uma interface adaptativa. Numa interface deste tipo, os elementos devem ser especialmente organizados em grupos que suportem automaticamente a posição relativa e as regras de dimensionamento.

Aqui nos deparamos com outra nuance da Biblioteca padrão, pois suas caixas de diálogo têm um tamanho fixo. No entanto, ao visualizar hipercubos OLAP grandes, para o usuário seria bom poder maximizar a janela ou, pelo menos, esticá-la o suficiente para ajustar os rótulos da célula nos eixos sem sobreposições.

O site mql5.com tem desenvolvimentos únicos e abertos no campo de interfaces gráficas que resolvem alguns dos problemas listados, mas a relação complexidade/capacidade está longe de ser comprometida: os recursos são limitados (por exemplo, há um mecanismo de disposição de elementos, mas não há um de escala) ou a integração requer muito esforço (ler documentação extensa, dominar formas não padronizadas de trabalho, etc.). Além disso, sendo todos os outros fatores iguais, é preferível usar uma solução baseada em elementos padrão.

Como resultado, escolhi como ponto de partida uma solução ponderada, na minha opinião, simples e sofisticada proposta nos artigos Usando layouts e containers para controles de gui: a classe cbox e Usando layouts e containers para controles da interface gráfica do usuário (GUI): a classe cgrid de Enrico Lambino.

No primeiro artigo, os controles são colocados em contêineres com disposição horizontal ou vertical e podem ser aninhados, o que permite um layout da interface completamente arbitrário. No segundo artigo, são propostos os contêineres com uma organização de tabela. Tanto o primeiro quanto o segundo funcionam com todos os controles padrão e funcionam com outros 'controles' escritos corretamente baseados em CWnd.

A única coisa que falta aqui é o redimensionamento dinâmico da janela e dos contêineres, este é o primeiro passo para resolver um problema comum.

Janelas de borracha

As classes CBox e CGrid são conectadas a projetos na forma de arquivos de cabeçalho Box.mqh, Grid.mqh, GridTk.mqh. Se você usar arquivos de artigos, esses arquivos devem ser instalados no diretório Include/Layouts.

Atenção! A Biblioteca padrão já possui a estrutura CGrid. Ele é projetada para desenhar grades nos gráficos. A classe contêiner CGrid não tem nada a ver com isso. A coincidência de nomes é desagradável, mas não crítica.

Vamos corrigir um pequeno erro no arquivo GridTk.mqh e complementar um pouco o arquivo Box.mqh, após o qual podemos continuar diretamente com o aprimoramento da classe da caixa de diálogo padrão — CAppDialog. Naturalmente, em vez de romper a classe existente, vamos criar uma nova derivada do CAppDialog.

A parte principal das alterações recai sobre o método CBox::GetTotalControlsSize (as linhas correspondentes são marcadas com comentários). Os interessados podem comparar os arquivos dos projetos originais com os arquivos anexados a este artigo.

  void CBox::GetTotalControlsSize(void)
  {
    m_total_x = 0;
    m_total_y = 0;
    m_controls_total = 0;
    m_min_size.cx = 0;
    m_min_size.cy = 0;
    int total = ControlsTotal();
    
    for(int i = 0; i < total; i++)
    {
      CWnd *control = Control(i);
      if(control == NULL) continue;
      if(control == &m_background) continue;
      CheckControlSize(control);
      
      // added: invoke itself recursively for nested containers
      if(control.Type() == CLASS_LAYOUT)
      {
        ((CBox *)control).GetTotalControlsSize();
      }
      
      CSize control_size = control.Size();
      if(m_min_size.cx < control_size.cx)
        m_min_size.cx = control_size.cx;
      if(m_min_size.cy < control_size.cy)
        m_min_size.cy = control_size.cy;
      
      // edited: m_total_x and m_total_y are incremeted conditionally according to container orientation
      if(m_layout_style == LAYOUT_STYLE_HORIZONTAL) m_total_x += control_size.cx;
      else m_total_x = MathMax(m_min_size.cx, m_total_x);
      if(m_layout_style == LAYOUT_STYLE_VERTICAL) m_total_y += control_size.cy;
      else m_total_y = MathMax(m_min_size.cy, m_total_y);
      m_controls_total++;
    }
    
    // added: adjust container size according to new totals
    CSize size = Size();
    if(m_total_x > size.cx && m_layout_style == LAYOUT_STYLE_HORIZONTAL)
    {
      size.cx = m_total_x;
    }
    if(m_total_y > size.cy && m_layout_style == LAYOUT_STYLE_VERTICAL)
    {
      size.cy = m_total_y;
    }
    Size(size);
  }

Em suma, a versão modificada leva em conta o possível redimensionamento dinâmico dos elementos.

Nos artigos originais, usados como exemplos de teste os EAs Controls2 (análogo do projeto Controls padrão que vem com o MetaTrader na pasta Experts\Examples\Controls\) SlidingPuzzle2. Ambos os exemplos de contêineres estão localizados por padrão na pasta Experts\Examples\Layouts\. É com base na base deles que vamos tentar implementar e testar janelas de borracha.

Criamos o arquivo MaximizableAppDialog.mqh em Include\Layouts\. Herdamos a classe de janela a partir de CAppDialog

  #include <Controls\Dialog.mqh>
  #include <Controls\Button.mqh>
  
  class MaximizableAppDialog: public CAppDialog
  {

Vamos precisar de 2 novos botões com imagens, quer dizer, um para maximizar a janela (estará localizado no cabeçalho, ao lado do botão minimizar) e outro para redimensionamento livre, no canto inferior direito.

  protected:
    CBmpButton m_button_truemax;
    CBmpButton m_button_size;

O sinal do atual estado maximizado ou processo de redimensionamento é armazenado nas variáveis lógicas correspondentes.

    bool m_maximized;
    bool m_sizing;

Também vamos adicionar um retângulo no qual constantemente vamos rastrear dinamicamente o tamanho do gráfico caso a janela esteja maximizada (ela precisa ser ajustada quando o gráfico for alterado), bem como um tamanho mínimo menor do que a janela não pode ser feita (o usuário pode definir esse limite usando o método público SetSizeLimit).

    CRect m_max_rect;
    CSize m_size_limit;

Os novos modos de maximização e redimensionamento adicionados devem interagir com os modos padrão, isto é, com o tamanho padrão e com a minimização da caixa de diálogo. Assim, se a janela estiver maximizada, ela não deve ser arrastada sobre o título, o que é permitido com o tamanho padrão. Além disso, o estado do botão de minimização deve ser redefinido quando a janela é maximizada. Para esses propósitos, precisamos acessar as variáveis CEdit m_caption na classe CDialog e CBmpButton m_button_minmax na classe CAppDialog. Infelizmente, eles e muitos outros membros dessas classes são declarados na seção private. Isso parece bastante estranho, dado que essas classes base fazem parte de uma biblioteca pública destinada a uso generalizado e expansão. De uma maneira amigável, todos os membros teriam que ser declarados protected ou pelo menos ter métodos para acessá-los. Aqui, este não é o caso, e não temos escolha senão consertar a biblioteca padrão aplicando um pequeno patch. Isso, claramente, é um problema, porque depois de atualizar a biblioteca, o patch precisa ser aplicado novamente, mas a única alternativa — criar classes duplicadas CDialog e CAppDialog — não parece estar correta do ponto de vista da ideologia OOP.

Este não é o último caso em que a declaração de membros nas classes base como private impede a expansão da funcionalidade em classes derivadas. A este respeito, propõe-se fazer uma cópia da pasta Include/Controls e, em caso de erros de compilação 'erro de acesso a membros privados', editar áreas problemáticas: transferir o elemento correspondente para a seção protegida ou simplesmente substituir private por protected.

Precisamos reescrever alguns dos métodos virtuais das classes base:

    virtual bool CreateButtonMinMax(void) override;
    virtual void OnClickButtonMinMax(void) override;
    virtual void Minimize(void) override;
  
    virtual bool OnDialogDragStart(void) override;
    virtual bool OnDialogDragProcess(void) override;
    virtual bool OnDialogDragEnd(void) override;

Os três primeiros estão associados ao botão minimizar, os três últimos, com o processo de redimensionamento, baseado na tecnologia drag'n'drop.

Também abordaremos os métodos virtuais de criar um diálogo e reagir aos eventos (o último é sempre implicitamente usado nas definições de macro do mapa de manipulação de eventos, que vamos discutir um pouco mais adiante).

    virtual bool Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2) override;
    virtual bool OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam) override;

O botão de maximizar é criado ao mesmo tempo com o botão padrão de minimização na versão substituída do CreateButtonMinMax. Neste ponto, é claro, a implementação básica é chamada primeiro para obter os botões de cabeçalho padrão e, em seguida, um novo botão de maximização é 'pintado' próximo dele. O código fonte é um conjunto usual de instruções para definir as coordenadas iniciais, o alinhamento, a conexão dos recursos da imagem e, portanto, não são fornecido aqui. Código fonte completo pode ser encontrado no anexo. Os recursos para os dois botões estão localizados no subdiretório "res":

  #resource "res\\expand2.bmp"
  #resource "res\\size6.bmp"
  #resource "res\\size10.bmp"

O método para lidar com cliques no botão de maximização é:

    virtual void OnClickButtonTrueMax(void);

Além disso, vamos adicionar métodos auxiliares para maximizar e restaurar a janela — eles realmente fazem todo o trabalho, sendo chamados a partir de OnClickButtonTrueMax dependendo de se a janela está maximizada ou não.

    virtual void Expand(void);
    virtual void Restore(void);

O botão de redimensionamento e a inicialização do processo de dimensionamento são criados nos seguintes métodos:

    bool CreateButtonSize(void);
    bool OnDialogSizeStart(void);

O tratamento de eventos é determinado por macros conhecidas:

  EVENT_MAP_BEGIN(MaximizableAppDialog)
    ON_EVENT(ON_CLICK, m_button_truemax, OnClickButtonTrueMax)
    ON_EVENT(ON_DRAG_START, m_button_size, OnDialogSizeStart)
    ON_EVENT_PTR(ON_DRAG_PROCESS, m_drag_object, OnDialogDragProcess)
    ON_EVENT_PTR(ON_DRAG_END, m_drag_object, OnDialogDragEnd)
  EVENT_MAP_END(CAppDialog)

Criamos os objetos m_button_truemax e m_button_size, mas m_drag_object é herdado da classe CWnd. Lá ele é usado para mover a janela sobre o cabeçalho. Em nossa classe, o mesmo objeto participa do redimensionamento da janela.

Mas isso não é todo trabalho com eventos. Para interceptar as alterações no tamanho do gráfico, precisamos lidar com o evento CHARTEVENT_CHART_CHANGE. Para este propósito, vamos descrever o método ChartEvent em nossa classe, pois ele vai se sobrepor ao método análogo em CAppDialog, e vamos precisar chamar a implementação base. No entanto, além disso, vamos verificar o código do evento e executar o processamento específico para CHARTEVENT_CHART_CHANGE.

  void MaximizableAppDialog::ChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
    if(id == CHARTEVENT_CHART_CHANGE)
    {
      if(OnChartChange(lparam, dparam, sparam)) return;
    }
    CAppDialog::ChartEvent(id, lparam, dparam, sparam);
  }

O método OnChartChange rastreia o tamanho do gráfico, e quando alterado com o modo de maximização ativa, inicia um novo layout de elementos, o que é obtido chamando o método especial SelfAdjustment.

  bool MaximizableAppDialog::OnChartChange(const long &lparam, const double &dparam, const string &sparam)
  {
    m_max_rect.SetBound(0, 0,
                        (int)ChartGetInteger(ChartID(), CHART_WIDTH_IN_PIXELS) - 0 * CONTROLS_BORDER_WIDTH,
                        (int)ChartGetInteger(ChartID(), CHART_HEIGHT_IN_PIXELS) - 1 * CONTROLS_BORDER_WIDTH);
    if(m_maximized)
    {
      if(m_rect.Width() != m_max_rect.Width() || m_rect.Height() != m_max_rect.Height())
      {
        Rebound(m_max_rect);
        SelfAdjustment();
        m_chart.Redraw();
      }
      return true;
    }
    return false;
  }

Na classe MaximizableAppDialog, esse método é definido como um virtual abstrato, ou seja, a classe herdada tem que ajustar seus 'controles' tendo em conta o novo tamanho.

    virtual void SelfAdjustment(const bool minimized = false) = 0;

O mesmo método é chamado de outros locais da classe de janela de borracha, onde ocorre o redimensionamento, por exemplo, de OnDialogDragProcess (quando o usuário arrasta o canto inferior direito) e OnDialogDragEnd (quando o usuário conclui o dimensionamento).

O comportamento do diálogo aprimorado é: depois de exibido no tamanho padrão no gráfico, o usuário pode movê-lo sobre o cabeçalho (comportamento padrão), minimizá-lo (comportamento padrão) e maximizar (comportamento adicionado). O estado maximizado é preservado quando o gráfico é redimensionado. A partir do estado maximizado, usando o mesmo botão, podemos retornar a janela ao seu tamanho original ou minimizá-la. A partir do estado minimizado, também podemos maximizar imediatamente a janela. Se a janela não estiver nem minimizada nem maximizada, a área ativa (botão triangular) é exibida no canto inferior direito para redimensionamento arbitrário. Se a janela estiver minimizada ou maximizada, esta área é desativada e ocultada.

A implementação de MaximizableAppDialog poderia ficar por aqui. No entanto, o processo de teste revelou outra nuance que exigiu um maior desenvolvimento.

Acontece que, no estado minimizado da janela, a área de redimensionamento ativa se sobrepõe ao botão para fechar a janela e intercepta os eventos do mouse a partir dela. Este é um erro óbvio da biblioteca, pois, no estado minimizado, o botão de redimensionamento está oculto e inativo. O problema está no método CWnd::OnMouseEvent. Ele não tem esta verificação:

  // if(!IS_ENABLED || !IS_VISIBLE) return false; - não tem esta linha

Como resultado, até os 'controles' desativados e invisíveis interceptaram eventos. Obviamente, o problema poderia ser resolvido definindo os elementos de controle de ordem Z apropriados. No entanto, aqui também foi descoberta uma falha — a biblioteca não leva em conta a ordem Z dos elementos. Em particular, se observarmos o método CWndContainer::OnMouseEvent, veremos um loop simples na ordem inversa em todos os elementos subordinados, sem tentar determinar sua prioridade na ordem Z.

Por essa razão, é necessário um patch de biblioteca adicional ou um certo 'truque' na classe derivada. Neste caso, escolhemos a segunda opção. O truque consiste em que, no estado minimizado, interpretamos pressionar o botão de redimensionamento como ao fechar a janela (já que é esse botão que se sobrepõe). Para fazer isso, à classe MaximizableAppDialog foi adicionado o botão:

  void MaximizableAppDialog::OnClickButtonSizeFixMe(void)
  {
    if(m_minimized)
    {
      Destroy();
    }
  }

E foi colocado no mapa de eventos:

  EVENT_MAP_BEGIN(MaximizableAppDialog)
    ...
    ON_EVENT(ON_CLICK, m_button_size, OnClickButtonSizeFixMe)
    ...
  EVENT_MAP_END(CAppDialog)

Agora a classe MaximizableAppDialog está realmente pronta para trabalhar. Note que é destinado para a área principal do gráfico.

Para começar, tentemos incorporá-lo na tag do racha-cuca. Depois de copiar SlidingPuzzle2.mq5 e SlidingPuzzle2.mqh para os arquivos com os próximos números SlidingPuzzle3.mq5 e SlidingPuzzle3.mqh, começamos a editá-los. Praticamente não é necessário alterar o arquivo mq5, basta substituir o link do arquivo de cabeçalho incluído por SlidingPuzzle3.mqh.

No arquivo SlidingPuzzle3.mqh, substituímos a conexão da classe de diálogo padrão pela recém-criada, isto é, em vez de:

  #include <Controls\Dialog.mqh>

Colocamos:

  #include <Layouts\MaximizableAppDialog.mqh>

A descrição da classe deve agora usar a nova classe pai:

  class CSlidingPuzzleDialog: public MaximizableAppDialog // CAppDialog

Além disso, uma substituição semelhante de nomes de classes deve ser feita no mapa de eventos:

  EVENT_MAP_END(MaximizableAppDialog) // CAppDialog

E também em mais um lugar, no método Create:

  bool CSlidingPuzzleDialog::Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
  {
    if(!MaximizableAppDialog::Create(chart, name, subwin, x1, y1, x2, y2)) // CAppDialog
      return (false);
    ...

Finalmente, o novo diálogo requer a implementação do método de Autoajuste, que responde ao redimensionamento.

  void CSlidingPuzzleDialog::SelfAdjustment(const bool minimized = false)
  {
    CSize size;
    size.cx = ClientAreaWidth();
    size.cy = ClientAreaHeight();
    m_main.Size(size);
    m_main.Pack();
  }

Aqui, delegamos todo o trabalho ao contêiner m_main chamando seu método Pack para o último tamanho conhecido da área de cliente da janela.

Isso é absolutamente suficiente para o jogo adquirir um layout adaptativo. No entanto, para melhorar a legibilidade e a eficiência do código, alterei o conceito de trabalhar com botões no aplicativo: eles agora são combinados no array único CButton m_buttons[16], acessíveis por índice, em vez de usar o operador switch e processados por uma única linha (usando o método OnClickButton) no mapa do evento:

  ON_INDEXED_EVENT(ON_CLICK, m_buttons, OnClickButton)

Os interessados podem comparar os códigos fonte do jogo original e os modificados.

É assim que a janela adaptável se comporta no gráfico.

Racha-Cuca

Racha-Cuca

Da mesma forma, é realizada a adaptação do EA de demonstração Experts\Examples\Layouts\Controls2.mq5 — seu arquivo mq5 principal e o arquivo de cabeçalho conectado com uma descrição da caixa de diálogo são apresentados sob novos nomes: Controls3.mq5 e ControlsDialog3.mqh. Deve-se notar que o jogo usa um contêiner do tipo tabela (grid), enquanto o diálogo com os controles é baseado nos painéis (box).

Se no projeto modificado deixarmos a mesma implementação do método SelfAdjustment como no racha-cuca, é fácil notar uma falha que permanece fora de nossa atenção: o redimensionamento adaptativo funciona até agora apenas na própria janela e não afeta os controles, mas seria bom poder ajustá-los dinamicamente sob o tamanho da janela.

Controles de borracha

Os controles padrão são adaptados de maneira diferente para alterar dinamicamente seu tamanho. Alguns, como os botões CButton, respondem corretamente a uma chamada do método Width. Para alguns, como as listas CListView, basta definir o alinhamento usando Alignment, e o sistema manterá automaticamente as lacunas entre o 'controle' e a borda da janela, o que equivale ao efeito borracha. No entanto, alguns elementos não suportam nenhum dos métodos. Entre eles estão, por exemplo, CSpinEdit ou CComboBox. Para dotá-los com essa nova habilidade, será necessário criar subclasses.

No caso do CSpinEdit, basta sobrescrever o método virtual OnResize:

  #include <Controls/SpinEdit.mqh> // patch required: private: -> protected:
  
  class SpinEditResizable: public CSpinEdit
  {
    public:
      virtual bool OnResize(void) override
      {
        m_edit.Width(Width());
        m_edit.Height(Height());
        
        int x1 = Width() - (CONTROLS_BUTTON_SIZE + CONTROLS_SPIN_BUTTON_X_OFF);
        int y1 = (Height() - 2 * CONTROLS_SPIN_BUTTON_SIZE) / 2;
        m_inc.Move(Left() + x1, Top() + y1);
        
        x1 = Width() - (CONTROLS_BUTTON_SIZE + CONTROLS_SPIN_BUTTON_X_OFF);
        y1 = (Height() - 2 * CONTROLS_SPIN_BUTTON_SIZE) / 2 + CONTROLS_SPIN_BUTTON_SIZE;
        m_dec.Move(Left() + x1, Top() + y1);
  
        return CWndContainer::OnResize();
      }
  };

Como o CSpinEdit consiste em 3 elementos — campos de entrada e dois botões “baloiço”, em resposta a um pedido de redimensionamento (que é o que faz o método OnResize) esticamos ou comprimimos o campo de entrada para um novo valor e movemos os botões para a borda direita do campo. O único problema é que os elementos subordinados — m_edit, m_inc, m_dec — são descritos na seção private. Assim, somos novamente confrontados com a necessidade de corrigir a biblioteca padrão. No entanto, o CSpinEdit foi necessário apenas para mostrar a abordagem, que, neste caso, é implementada de forma muito simples. Já para a interface OLAP real, precisamos de uma lista suspensa adaptada.

Mas um problema similar surge no caso de customização da classe CComboBox. Antes de implementar uma classe derivada, precisamos aplicar um patch ao CComboBox base, substituindo private por protected. Observe que todos esses patches não afetam a compatibilidade com outros projetos usando a biblioteca padrão.

A implementação do efeito borracha da caixa de combinação requer um pouco mais de esforço. Aqui você precisa substituir não apenas OnResize, mas também OnClickButton, Enable, Disable e até mesmo adicionar um mapa de eventos. Temos que gerenciar os objetos subordinados m_edit, m_list, m_drop, isto é, tudo no qual consiste a caixa de combinação.

  #include <Controls/ComboBox.mqh> // patch required: private: -> protected:
  
  class ComboBoxResizable: public CComboBox
  {
    public:
      virtual bool OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam) override;
  
      virtual bool OnResize(void) override
      {
        m_edit.Width(Width());
        
        int x1 = Width() - (CONTROLS_BUTTON_SIZE + CONTROLS_COMBO_BUTTON_X_OFF);
        int y1 = (Height() - CONTROLS_BUTTON_SIZE) / 2;
        m_drop.Move(Left() + x1, Top() + y1);
        
        m_list.Width(Width());
  
        return CWndContainer::OnResize();
      }
      
      virtual bool OnClickButton(void) override
      {
        // this is a hack to trigger resizing of elements in the list
        // we need it because standard ListView is incorrectly coded in such a way
        // that elements are resized only if vscroll is present
        bool vs = m_list.VScrolled();
        if(m_drop.Pressed())
        {
          m_list.VScrolled(true);
        }
        bool b = CComboBox::OnClickButton();
        m_list.VScrolled(vs);
        return b;
      }
      
      virtual bool Enable(void) override
      {
        m_edit.Show();
        m_drop.Show();
        return CComboBox::Enable();
      }
      
      virtual bool Disable(void) override
      {
        m_edit.Hide();
        m_drop.Hide();
        return CComboBox::Disable();
      }
  };
  
  #define EXIT_ON_DISABLED \
        if(!IsEnabled())   \
        {                  \
          return false;    \
        }
  
  EVENT_MAP_BEGIN(ComboBoxResizable)
    EXIT_ON_DISABLED
    ON_EVENT(ON_CLICK, m_drop, OnClickButton)
  EVENT_MAP_END(CComboBox)

Depois de termos controles de borracha, podemos verificá-los no projeto de demonstração Controls3. Substituímos as classes CSpinEdit e CComboBox por SpinEditResizable e ComboBoxResizable, respectivamente. No método de Autoajuste, alteramos o tamanho dos 'controles'.

  void CControlsDialog::SelfAdjustment(const bool minimized = false)
  {
    CSize min = m_main.GetMinSize();
    CSize size;
    size.cx = ClientAreaWidth();
    size.cy = ClientAreaHeight();
    if(minimized)
    {
      if(min.cx > size.cx) size.cx = min.cx;
      if(min.cy > size.cy) size.cy = min.cy;
    }
    m_main.Size(size);
    int w = (m_button_row.Width() - 2 * 2 * 2 * 3) / 3;
    m_button1.Width(w);
    m_button2.Width(w);
    m_button3.Width(w);
    m_edit.Width(w);
    m_spin_edit.Width(w);
    m_combo_box.Width(m_lists_row.Width() / 2);
    m_main.Pack();
  }

O método SelfAdjustment será chamado pela classe pai MaximizableAppDialog depois de redimensionar a janela automaticamente. Além disso, nós mesmos chamaremos esse método uma vez no momento da inicialização da janela, a partir do método CreateMain.

Em andamento, isso pode ser algo como isto (aqui, por simplicidade, os 'controles' preencheram a área de trabalho da janela apenas horizontalmente, mas o mesmo efeito pode ser aplicado verticalmente).

Demonstração dos controles

Demonstração dos controles

Bordas vermelhas são exibidas para fins de depuração — elas são desativadas usando a macro LAYOUT_BOX_DEBUG.

Além das mudanças acima, também modifiquei ligeiramente o princípio de inicialização dos 'controles'. Cada bloco, começando com a área de cliente principal da janela, é totalmente inicializado num método separado (por exemplo, CreateMain, CreateEditRow, CreateButtonRow, etc.), que retorna uma referência ao tipo de contêiner criado (CWnd*), se bem-sucedida. O contêiner pai adiciona o subordinado chamando CWndContainer::Add. Aqui está o método principal de inicialização do diálogo:

  bool CControlsDialog::Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
  {
      if(MaximizableAppDialog::Create(chart, name, subwin, x1, y1, x2, y2)
      && Add(CreateMain(chart, name, subwin)))
      {
          return true;
      }
      return false;
  }
  
  CWnd *CControlsDialog::CreateMain(const long chart, const string name, const int subwin)
  {
      m_main.LayoutStyle(LAYOUT_STYLE_VERTICAL);
      if(m_main.Create(chart, name + "main", subwin, 0, 0, ClientAreaWidth(), ClientAreaHeight())
      && m_main.Add(CreateEditRow(chart, name, subwin))
      && m_main.Add(CreateButtonRow(chart, name, subwin))
      && m_main.Add(CreateSpinDateRow(chart, name, subwin))
      && m_main.Add(CreateListsRow(chart, name, subwin))
      && m_main.Pack())
      {
          SelfAdjustment();
          return &m_main;
      }
      return NULL;
  }

Esta é a inicialização de uma linha com botões:

  CWnd *CControlsDialog::CreateButtonRow(const long chart, const string name, const int subwin)
  {
      if(m_button_row.Create(chart, name + "buttonrow", subwin, 0, 0, ClientAreaWidth(), BUTTON_HEIGHT * 1.5)
      && m_button_row.Add(CreateButton1())
      && m_button_row.Add(CreateButton2())
      && m_button_row.Add(CreateButton3()))
      {
        m_button_row.Alignment(WND_ALIGN_LEFT|WND_ALIGN_RIGHT, 2, 0, 2, 0);
        return &m_button_row;
      }
      return NULL;
  }

Essa sintaxe parece mais lógica e compacta do que a usada anteriormente, mas pode dificultar a comparação contextual de projetos antigos e novos — tenha isso em mente.

Nossa epopeia com controles, infelizmente, não termina aí. Lembre-se de que o objetivo do projeto é fornecer uma interface gráfica para o OLAP. Portanto, o controle gráfico deve ocupar um lugar central. O problema é que simplesmente não existe tal elemento na biblioteca padrão. Ele terá que ser criado.

Controle gráfico (CPlot)

A biblioteca MQL fornece várias primitivas gráficas. Entre elas, tela de desenho (CCanvas), gráfico de negociação baseado em telas (CGraphic) e, finalmente, objetos gráficos para exibir imagens prontas (CChartObjectBitmap, CPicture) — infelizmente, elas não estão relacionados a gráficos de negociação. Para incorporá-las na interface da janela, precisamos incluir uma das opções mencionadas na classe de herança do controle que pode ser desenhada. Felizmente, essa tarefa não precisa ser resolvida do zero. No site publicado o artigo Gráfico PairPlot baseado em CGraphic para analisar correlações entre arrays de dados (séries temporais). Ele propõe uma classe de controle pronta que inclui um conjunto de gráficos para analisar dependências pareadas entre vários símbolos. Assim, basta modificá-la para trabalhar com um único gráfico no 'controle' e assim obter o resultado desejado.

Os códigos fonte do artigo são instalados no diretório Include\PairPlot\. O arquivo que contém a classe na qual estamos interessados é chamado PairPlot.mqh. Com base nisso, criaremos nossa própria versão, sob o nome Plot.mqh. As principais diferenças serão as seguintes.

Nós não precisamos da classe CTimeserie, portanto vamos removê-la. A classe CPairPlot, que é um 'controle' derivado de CWndClient, é transformada em CPlot, substituindo nela todo o trabalho com o array de gráficos de pares de moedas por um único gráfico. Diretamente gráficos nesse projeto são desenhados usando classes de histograma especiais (CHistogram) e diagramas de dispersão (CScatter), que são derivados da classe base geral CPlotBase, que por sua vez é um sucessor de CGraphic. Nós converteremos CPlotBase na nossa própria classe CGraphicInPlot, também herdada de CGraphic, e prescindimos de histogramas especiais e gráficos de dispersão. Em vez disso, usaremos estilos de desenho padrão (CURVE_POINTS, CURVE_LINES, CURVE_POINTS_AND_LINES, CURVE_STEPS, CURVE_HISTOGRAM) fornecidos pela classe CGraphic. Um diagrama simplificado de relação de classe é mostrado abaixo.

Diagrama de relação de classe gráficas

Diagrama de relação de classe gráficas

As classes adicionadas são destacadas em cinza, o resto é padrão.

Para testar o funcionamento do novo controle, criaremos o EA de teste PlotDemo. Como de costume, a inicialização, a ligação a eventos e a ativação são executadas no arquivo PlotDemo.mq5 e a descrição da caixa de diálogo é feita em PlotDemo.mqh (os dois arquivos estão anexados).

O EA aceita um único parâmetro de entrada — o estilo de desenho.

  #include "PlotDemo.mqh"
  
  input ENUM_CURVE_TYPE PlotType = CURVE_POINTS;
  
  CPlotDemo *pPlotDemo;
  
  int OnInit()
  {
      pPlotDemo = new CPlotDemo;
      if(CheckPointer(pPlotDemo) == POINTER_INVALID) return INIT_FAILED;
  
      if(!pPlotDemo.Create(0, "Plot Demo", 0, 20, 20, 800, 600, PlotType)) return INIT_FAILED;
      if(!pPlotDemo.Run()) return INIT_FAILED;
      pPlotDemo.Refresh();
  
      return INIT_SUCCEEDED;
  }
  
  void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
      pPlotDemo.ChartEvent(id, lparam, dparam, sparam);
  }
  
  ...

No arquivo de cabeçalho do diálogo, criamos o objeto do nosso 'controle' e adicionamos duas curvas de teste.

  #include <Controls\Dialog.mqh>
  #include <PairPlot/Plot.mqh>
  #include <Layouts/MaximizableAppDialog.mqh>
  
  class CPlotDemo: public MaximizableAppDialog // CAppDialog
  {
    private:
      CPlot m_plot;
  
    public:
      CPlotDemo() {}
      ~CPlotDemo() {}
  
      bool Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2, const ENUM_CURVE_TYPE curveType = CURVE_POINTS);
      virtual bool OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam);
      bool Refresh(void);
  
      virtual void SelfAdjustment(const bool minimized = false) override
      {
        if(!minimized)
        {
          m_plot.Size(ClientAreaWidth(), ClientAreaHeight());
          m_plot.Resize(0, 0, ClientAreaWidth(), ClientAreaHeight());
        }
        m_plot.Refresh();
      }
  };
  
  EVENT_MAP_BEGIN(CPlotDemo)
  EVENT_MAP_END(MaximizableAppDialog)
  
  bool CPlotDemo::Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2, const ENUM_CURVE_TYPE curveType = CURVE_POINTS)
  {
      const int maxw = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
      const int maxh = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
      int _x1 = x1;
      int _y1 = y1;
      int _x2 = x2;
      int _y2 = y2;
      if(x2 - x1 > maxw || x2 > maxw)
      {
        _x1 = 0;
        _x2 = _x1 + maxw - 0;
      }
      if(y2 - y1 > maxh || y2 > maxh)
      {
        _y1 = 0;
        _y2 = _y1 + maxh - 1;
      }
      
      if(!MaximizableAppDialog::Create(chart, name, subwin, _x1, _y1, _x2, _y2))
          return false;
      if(!m_plot.Create(m_chart_id, m_name + "Plot", m_subwin, 0, 0, ClientAreaWidth(), ClientAreaHeight(), curveType))
          return false;
      if(!Add(m_plot))
          return false;
      double x[] = {-10, -4, -1, 2, 3, 4, 5, 6, 7, 8};
      double y[] = {-5, 4, -10, 23, 17, 18, -9, 13, 17, 4};
      m_plot.CurveAdd(x, y, "Example 1");
      m_plot.CurveAdd(y, x, "Example 2");
      return true;
  }
  
  bool CPlotDemo::Refresh(void)
  {
      return m_plot.Refresh();
  }

Na animação a seguir pode ser visto como funciona este EA:

Controle com gráfico de negociação

Controle com gráfico de negociação

Assim, nós fizemos muito trabalho e agora as possibilidades de construir uma interface adaptativa com suporte gráfico atendem aos nossos requisitos no âmbito do projeto OLAP. Para resumir, apresentamos um diagrama das principais classes associadas à interface gráfica do usuário.

Diagrama de classe de controles

Diagrama de classe de controles

A cor branca indica classes padrão, amarelo - classes de contêiner, rosa - diálogo e elementos personalizados que suportam redimensionamento, verde - 'controle' com gráfico de negociação incorporado.

GUI para OLAP

O processamento interativo dos dados do histórico de negociação e sua visualização será tratado por um novo EA - OLAPGUI. Todas as operações para criar uma janela, 'controles', reações a ações do usuário e chamar funções OLAP estão contidas no arquivo de cabeçalho OLAPGUI.mqh.

Nas variáveis de entrada do EA, deixamos apenas o que está relacionado à importação de dados de HTML ou CSV. Primeiro, isso diz respeito às variáveis ReportFile, Prefix, Suffix que já conhecemos desde o primeiro projeto OLAPDEMO. Se ReportFile for deixado em branco, será analisado o histórico da conta atual.

Diretamente, usaremos controles para implementar a escolha de seletores, de agregadores e de estilo gráfico. Como antes, deixamos a possibilidade de especificar 3 dimensões do hipercubo, ou seja, 3 seletores ao longo dos eixos condicionais X, Y, Z. Isso exigirá 3 listas suspensas. Vamos colocá-las na primeira linha superior dos 'controles'. Na mesma linha na borda direita, faremos o botão Process, clicando nele será iniciada a análise.

A escolha da função de agregador e de campo é implementada usando duas outras listas suspensas — na segunda linha dos 'controles'. No mesmo local, adicionamos uma lista suspensa para a ordem de classificação e para o estilo do gráfico. A filtragem é excluída do projeto para simplificar a interface.

Todo o espaço restante na janela será ocupado pelo gráfico.

Listas suspensas com seletores conterão o mesmo conjunto de opções. Nela, combinamos os tipos de seletores e, diretamente, os campos de registros exibidos. A tabela a seguir lista os nomes de elementos e seus campos e/ou tipos de seletores correspondentes.

A escolha dos seletores marcados com * é determinada pelo tipo de agregador: no caso de IdentityAggregator, é usado TradeSelector, caso contrário, QuantizationSelector.

Os nomes dos seletores (itens 1 a 9) na lista suspensa estão entre aspas.

Os seletores devem ser selecionados sequencialmente da esquerda para a direita, de X para Z. Até que o seletor de medição anterior seja selecionado, as caixas de combinação dos eixos subsequentes ficam ocultas.

Funções agregadas suportadas:

Todas, exceto a último, exigem a especificação do campo de registro a ser agregado usando a lista suspensa à direita do agregador.

A função 'progressive total' implica que o seletor no eixo X é selecionado como 'ordinal'.

A caixa de combinação com ordenação só está disponível se selecionado o único seletor (X).

Os eixos X e Y estão localizados no gráfico horizontal e verticalmente, respectivamente. Para hipercubos tridimensionais com diferentes coordenadas ao longo do eixo Z, foi aplicada a abordagem mais primitiva possível: várias seções no plano Z podem ser roladas usando o botão Process. Se houver coordenadas Z, o botão muda o nome para 'i / n title >>', onde i é o número da coordenada Z atual, n é o número total de amostras ao longo do eixo Z, title é o nome da contagem (por exemplo, o dia da semana ou o tipo de transações dependendo do seletor selecionado no eixo Z). Se alterarmos as condições para construir o hipercubo, o botão novamente receberá o cabeçalho 'Process' e começará a funcionar no modo normal. Observe que quando o agregador de 'identity' funciona, o processamento é diferente, quer dizer, nesse caso, o cubo sempre tem 2 dimensões e todas as três curvas (para os campos X, Y, Z) são exibidas juntas no gráfico, sem rolagem.

Cada cubo é exibido graficamente e também é mostrado como texto no log. Isso é especialmente importante para os casos em que a agregação é executada em campos simples e não em seletores. Os seletores fornecem exibição de rótulos ao longo dos eixos e, ao quantificar um campo simples, o sistema só pode mostrar o índice da célula. Por exemplo, se quisermos analisar o lucro discriminado por tamanho de lote, podemos selecionar o campo 'lot' no seletor X e o agregador 'sum' do campo 'profit amount'. Neste caso, as contagens de 0, 0.5, 1, 1.0, 1.5, etc. podem aparecer no eixo X até o número de diferentes volumes em que realizada a negociação. No entanto, eles serão os números das células, e não, os valores dos lotes - o último pode ser visto no log. Ele mostrará a seguinte entrada:

	Selectors: 1
	SumAggregator<TRADE_RECORD_FIELDS> FIELD_PROFIT_AMOUNT [6]
	X: QuantizationSelector(FIELD_LOT) [6]
	===== QuantizationSelector(FIELD_LOT) =====
	      [value] [title]
	[0] 365.96000 "0.01"
	[1]   0.00000 "0.0"
	[2]   4.65000 "0.03"
	[3]  15.98000 "0.06"
	[4]  34.23000 "0.02"
	[5]   0.00000 "1.0"

Aqui, value é o lucro total, title é o tamanho real do lote correspondente a este lucro e os números à esquerda são as coordenadas no eixo X. Observe que só valores fracionários podem ser exibidos no gráfico ao longo do eixo, embora apenas índices inteiros façam sentido. Este e outros aspectos da marcação podem certamente ser melhorados.

Para vincular os controles da GUI com o núcleo OLAP (que deixaremos inalterado, como no primeiro artigo no arquivo de cabeçalho OLAPcube.mqh) precisaremos implementar a classe de camada OLAPWrapper. Ela realmente realizará o mesmo trabalho preparatório com dados feito pela função process no primeiro projeto de demonstração OLAPDEMO, mas agora isso será um método de classe.

  class OLAPWrapper
  {
    protected:
      Selector<TRADE_RECORD_FIELDS> *createSelector(const SELECTORS selector, const TRADE_RECORD_FIELDS field);
  
    public:
      void process(
          const SELECTORS &selectorArray[], const TRADE_RECORD_FIELDS &selectorField[],
          const AGGREGATORS AggregatorType, const TRADE_RECORD_FIELDS AggregatorField, Display &display,
          const SORT_BY SortBy = SORT_BY_NONE,
          const double Filter1value1 = 0, const double Filter1value2 = 0)
      {
        int selectorCount = 0;
        for(int i = 0; i < MathMin(ArraySize(selectorArray), 3); i++)
        {
          selectorCount += selectorArray[i] != SELECTOR_NONE;
        }
        ...
        HistoryDataAdapter<CustomTradeRecord> history;
        HTMLReportAdapter<CustomTradeRecord> report;
        CSVReportAdapter<CustomTradeRecord> external;
        
        DataAdapter *adapter = &history;
        
        if(ReportFile != "")
        {
          if(StringFind(ReportFile, ".htm") > 0 && report.load(ReportFile))
          {
            adapter = &report;
          }
          else
          if(StringFind(ReportFile, ".csv") > 0 && external.load(ReportFile))
          {
            adapter = &external;
          }
          else
          {
            Alert("Unknown file format: ", ReportFile);
            return;
          }
        }
        else
        {
          Print("Analyzing account history");
        }
        
        Selector<TRADE_RECORD_FIELDS> *selectors[];
        ArrayResize(selectors, selectorCount);
        
        for(int i = 0; i < selectorCount; i++)
        {
          selectors[i] = createSelector(selectorArray[i], selectorField[i]);
        }
  
        Aggregator<TRADE_RECORD_FIELDS> *aggregator;
        switch(AggregatorType)
        {
          case AGGREGATOR_SUM:
            aggregator = new SumAggregator<TRADE_RECORD_FIELDS>(AggregatorField, selectors, filters);
            break;
            ...
        }
        
        Analyst<TRADE_RECORD_FIELDS> *analyst;
        analyst = new Analyst<TRADE_RECORD_FIELDS>(adapter, aggregator, display);
        
        analyst.acquireData();
        ...
        analyst.build();
        analyst.display(SortBy, AggregatorType == AGGREGATOR_IDENTITY);
        ...
      }

Código fonte completo pode ser encontrado no anexo. Observe que todas as configurações que recebemos no projeto OLAPDEMO das variáveis de entrada agora são passadas como parâmetros do método process e, obviamente, devem ser preenchidas com base no estado dos controles.

O parâmetro display é de particular interesse. O núcleo do OLAP declara essa interface especial display para visualização de dados, já nós precisamos implementá-la na parte gráfica do programa. Depois de criar um objeto com essa interface, injetaremos uma dependência (dependency injection), discutida no primeiro artigo. Isso permitirá conectar uma nova maneira de exibir resultados ao núcleo do OLAP sem alterá-lo.

No arquivo OLAPGUI.mq5, criamos um diálogo, passando uma instância do OLAPWrapper para ele.

  #include "OLAPGUI.mqh"
  
  OLAPWrapper olapcore;
  OLAPDialog dialog(olapcore);
  
  int OnInit()
  {
      if(!dialog.Create(0, "OLAPGUI" + (ReportFile != "" ? " : " + ReportFile : ""), 0,  0, 0, 584, 456)) return INIT_FAILED;
      if(!dialog.Run()) return INIT_FAILED;
      return INIT_SUCCEEDED;
  }
  ...

A classe de diálogo OLAPDialog é definida em OLAPGUI.mqh.

  class OLAPDialog;
  
  // since MQL5 does not support multiple inheritence we need this delegate object
  class OLAPDisplay: public Display
  {
    private:
      OLAPDialog *parent;
  
    public:
      OLAPDisplay(OLAPDialog *ptr): parent(ptr) {}
      virtual void display(MetaCube *metaData, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override;
  };
  
  class OLAPDialog: public MaximizableAppDialog
  {
    private:
      CBox m_main;
  
      CBox m_row_1;
      ComboBoxResizable m_axis[AXES_NUMBER];
      CButton m_button_ok;
  
      CBox m_row_2;
      ComboBoxResizable m_algo[ALGO_NUMBER]; // aggregator, field, graph type, sort by
  
      CBox m_row_plot;
      CPlot m_plot;
      ...
      OLAPWrapper *olapcore;
      OLAPDisplay *olapdisplay;
      ...
  
    public:
      OLAPDialog(OLAPWrapper &olapimpl)
      {
        olapcore = &olapimpl;
        olapdisplay = new OLAPDisplay(&this);
      }
      
      ~OLAPDialog(void);
      ...

Em resposta a pressionar o botão 'Process', um diálogo baseado na posição dos 'controles' preenche os parâmetros necessários para o método OLAPWrapper::process e o chama, passando o objeto olapdisplay como um display:

  void OLAPDialog::OnClickButton(void)
  {
    SELECTORS Selectors[4];
    TRADE_RECORD_FIELDS Fields[4];
    AGGREGATORS at = (AGGREGATORS)m_algo[0].Value();
    TRADE_RECORD_FIELDS af = (TRADE_RECORD_FIELDS)(AGGREGATORS)m_algo[1].Value();
    SORT_BY sb = (SORT_BY)m_algo[2].Value();
  
    ArrayInitialize(Selectors, SELECTOR_NONE);
    ArrayInitialize(Fields, FIELD_NONE);
    ...
    
    olapcore.process(Selectors, Fields, at, af, olapdisplay, sb);
  }

O código completo para definir todos os parâmetros pode ser visualizado no apêndice.

Precisamos da classe auxiliar OLAPDisplay porque o MQL não suporta herança múltipla. A classe OLAPDialog é derivada de MaximizableAppDialog e, portanto, não pode implementar diretamente a interface Dialog. Em vez disso, delegamos essa tarefa à classe OLAPDisplay e criamos seu objeto dentro da janela e fornecemos uma referência ao criador por meio do parâmetro do construtor.

Depois de construir o cubo, o núcleo OLAP chama o método OLAPDisplay::display:

  void OLAPDisplay::display(MetaCube *metaData, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override
  {
    int consts[];
    int selectorCount = metaData.getDimension();
    ArrayResize(consts, selectorCount);
    ArrayInitialize(consts, 0);
  
    Print(metaData.getMetaCubeTitle(), " [", metaData.getCubeSize(), "]");
    for(int i = 0; i < selectorCount; i++)
    {
      Print(CharToString((uchar)('X' + i)), ": ", metaData.getDimensionTitle(i), " [", metaData.getDimensionRange(i), "]");
    }
    
    if(selectorCount == 1)
    {
      PairArray *result;
      if(metaData.getVector(0, consts, result, sortby))
      {
        Print("===== " + metaData.getDimensionTitle(0) + " =====");
        ArrayPrint(result.array);
        parent.accept1D(result, metaData.getDimensionTitle(0));
      }
      parent.finalize();
      return;
    }
    ...

A essência do que está acontecendo é obter os dados (getDimension(), getDimensionTitle(), getVector()) do objeto metaData e passá-los para a janela. No fragmento acima, pode ser visto o processamento do caso com um único seletor. Na classe de nosso diálogo, para receber dados são reservados métodos especiais:

  void OLAPDialog::accept1D(const PairArray *data, const string title)
  {
    m_plot.CurveAdd(data, title);
  }
  
  void OLAPDialog::accept2D(const double &x[], const double &y[], const string title)
  {
    m_plot.CurveAdd(x, y, title);
  }
  
  void OLAPDialog::finalize()
  {
    m_plot.Refresh();
    m_button_ok.Text("Process");
  }

Aqui estão alguns exemplos de alguns perfis analíticos que podem ser obtidos graficamente usando o OLAPGUI.

Lucro por símbolo em ordem decrescente

Lucro por símbolo em ordem decrescente

Lucro por símbolo em ordem alfabética

Lucro por símbolo em ordem alfabética

Lucro por símbolo, por dia da semana de fechamento, tipo 'compra'

Lucro por símbolo, por dia da semana de fechamento, tipo 'compra'

Lucro por símbolo, por dia da semana de fechamento, tipo 'venda'

Lucro por símbolo, por dia da semana de fechamento, tipo 'venda'

Lucro por tamanho do lote (lotes são indicados como índices de célula, valores são exibidos no log)

Lucro por tamanho do lote (lotes são indicados como índices de célula, valores são exibidos no log)

Curva de saldo total

Curva de saldo total

Saldo por compras e vendas

Saldo por compras e vendas

Curvas de saldo para cada símbolo separadamente

Curvas de saldo para cada símbolo separadamente

Curvas de swaps para cada símbolo separadamente

Curvas de swaps para cada símbolo separadamente

Dependência de lucros sobre a duração do trade para cada símbolo separadamente

Dependência de lucros sobre a duração do trade para cada símbolo separadamente

Número de trades por símbolo e por tipo

Número de trades por símbolo e por tipo

Dependência dos campos de lucro e duração para cada trade (duração especificada em segundos)

Dependência dos campos de lucro e duração para cada trade (duração especificada em segundos)

Dependência MFE (%) e MAE (%) para todos os trades

Dependência MFE (%) e MAE (%) para todos os trades

Infelizmente, o estilo padrão de desenho de histogramas não fornece uma exibição de vários arrays com deslocamento de colunas do mesmo índice a partir de diferentes arrays. Em outras palavras, valores com a mesma coordenada podem se sobrepor completamente. Para resolver este problema, é necessário implementar um método pessoal de visualização de histograma (permitindo criar a classe CGraphic), mas isso é deixado para trabalho independente.

Fim do artigo

Neste artigo, revisamos os princípios gerais de criação de uma interface gráfica do usuário para programas MQL que suportam redimensionamento e disposição universal de controles. Com base nessa abordagem, foi criada uma aplicação interativa para a análise de relatórios de negociação usando os recursos do primeiro artigo da série OLAP. A visualização de vários indicadores em combinações e pontos de vista arbitrários permite identificar padrões ocultos e simplifica a análise de critérios múltiplos para otimizar os sistemas de negociação.

A seguir estão as listas de arquivos de projeto anexados.

Projeto OLAPGUI

Projeto SlidingPuzzle3

Projeto Controls3

Projeto PlotDemo