Implementando OLAP en la negociación (Parte 2): Visualización de los resultados del análisis interactivo de los datos multidimensionales

Stanislav Korotky | 22 julio, 2019

En el primer artículo dedicado a OLAP en la negociación, consideramos los principios generales del procesamiento analítico de datos multidimensionales y propusimos las clases MQL hechas, que permitían aplicar OLAP en la práctica para el historial de la cuenta o para los informes comerciales. Al mismo tiempo, la visualización de los resultados del análisis fue implementada por ahora en un formato de texto simplificado, en el log de los Asesores Expertos (EA). Para mejorar el formato visual, hay que crear una clase nueva, un heredero de la interfaz Display, capaz de visualizar los datos OLAP a través de la gráfica del trading. Como resultó, esta tarea requería un gran trabajo preparativo y abarcaba muchos aspectos diferentes no relacionados con OLAP. Así que, por ahora, vamos a dejar de lado el procesamiento de datos, y examinaremos gradualmente, paso a paso, la interfaz del programa MQL.

Para implementar una interfaz gráfica en MQL, existen varias bibliotecas, inclusive la Biblioteca estándar de los controles (Include/Controls). Una de las importantes omisiones, prácticamente en todas las bibliotecas, se debe al hecho de que no hay métodos de controlar automáticamente la disposición de los controles dentro de la ventana. En otras palabras, el posicionamiento y la alineación de los controles se realiza estáticamente usando la codificación rígida (hard code) de las constantes con las coordenadas X y Y. Este problema está relacionada estrechamente con el otro, es decir, con la falta de los medios del diseño visual de los formularios de pantalla. Este problema es aún más complicado, aunque puede ser solucionado. Pero como la interfaz no era el tema principal en este proyecto, decidimos no prestar atención al editor de los formularios de pantalla y centrarse en un enfoque más simple con la implementación de una interfaz adaptable. En la interfaz de este tipo, los controles tienen que estar organizados de una manera especial en grupos que soporten automáticamente la posición relativa y las reglas del escalamiento.

Aquí nos encontramos con otro detalle de la Biblioteca estándar: sus ventanas de diálogo tienen un tamaño fijo. No obstante, al visualizar los hipercubos OLAP grandes, para el usuario sería conveniente tener la posibilidad de maximizar la ventana, o por lo menos estirarla suficientemente para que las etiquetas de las células se ajusten en los ejes sin superposición.

En el sitio mql5.com hay desarrollos únicos abiertos en el campo de las interfaces gráficas que resuelven algunos problemas mencionados, pero su relación «complejidad / posibilidades» está lejos de ser de compromiso: los recursos están limitados (por ejemplo, hay un mecanismo del disposición de los controles, pero no hay posibilidad de escalamiento), o la integración requiere muchos esfuerzos (leer la documentación extensa, dominar las formas no estándar del trabajo, etc.). Además, cuando otros factores son iguales, es preferible usar una solución a base de los elementos estándar, como los más típicos (es decir, los elementos que se usan en la mayoría de los programas MQL, y por tanto, con el mayor coeficiente de utilidad).

Al final, elegí como punto de partida una solución ponderada (según mi opinión), simple y sofisticada que fue propuesta en los artículos Aplicación de los contenedores para componer la interfaz gráfica: clase CBox y Aplicación de los contenedores para componer la interfaz gráfica: clase CGrid de Enrico Lambino.

En primer artículo los controles se colocan en los contenedores con la disposición horizontal o vertical, pudiendo ser añidados, lo que permite conseguir un planeamiento de la interfaz absolutamente aleatoria. En el segundo artículo, se proponen los contenedores con una organización de la tabla. Tanto los primeros como los segundos funcionan con todos los controles estándar, pudiendo funcionar también con otros controles escritos correctamente a base de CWnd.

La única cosa que falta aquí es el redimencionamiento dinámico de la pantalla y de los contenedores. Este será el primer paso para resolver el problema común.

Ventanas «de goma»

Las clases CBox y CGrid se conectan a los proyectos como los archivos de cabecera Box.mqh, Grid.mqh, GridTk.mqh. Si Usted usa los archivos de los artículos, hay que colocarlos en la carpeta Include/Layouts.

¡Atención! La Biblioteca estándar ya contiene la estructura CGrid. Se usa para dibujar la cuadrícula en los gráficos. La clase del contenedor CGrid no tiene nada que ver con eso. La coincidencia de los nombres es desagradable pero no es crítica.

Vamos a corregir un pequeño error en el archivo GridTk.mqh y completaremos un poco el archivo Box.mqh, después de eso, podemos proceder directamente al mejoramiento de la clase estándar de los diálagos CAppDialog. Está claro que no vamos a romper la clase existente, sino vamos a crear una clase nueva derivada de CAppDialog.

La parte principal de las modificaciones recae sobre el método CBox::GetTotalControlsSize (las líneas correspondientes están marcadas con los comentarios). El que está interesado puede comparar los archivos de los proyectos originales con los archivos adjuntos al presente artículo.

  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);
  }

En pocas palabras, la versión modificada considera una posible alteración dinámica del tamaño de los controles.

En los artículos originales, como ejemplos, se usaban los Asesores Expertos Controls2 (análogo del proyecto estándar Controls que viene con MetaTrader en la carpeta Experts\Examples\Controls\) y SlidingPuzzle2. Ambos ejemplos de los contenedores se ubican por defecto en la carpeta Experts\Examples\Layouts\. Precisamente a su base, trataremos de implementar y testear las ventanas de «goma».

Creamos el archivo MaximizableAppDialog.mqh en Include\Layouts\. Heredamos la clase de la ventana de CAppDialog

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

Necesitaremos 2 nuevos botones con imágenes: un botón para maximizar la ventana (estará ubicado en el encabezado al lado del botón para minimizar), y otro para el cambio aleatorio del tamaño (en la esquina inferior derecha).

  protected:
    CBmpButton m_button_truemax;
    CBmpButton m_button_size;

Vamos a guardar la señal del estado maximizado actual o del proceso del cambio del tamaño en las variables lógicas correspondientes.

    bool m_maximized;
    bool m_sizing;

Además, vamos a añadir un rectángulo en el cual vamos a rastrear constantemente el tamaño del gráfico para el caso si la ventana está maximizada (y hay que ajustarla cuando el gráfico se cambia), así como un tamaño mínimo menor del cual no se puede hacer la ventana (el usuario puede definir esta limitación usando el método público SetSizeLimit).

    CRect m_max_rect;
    CSize m_size_limit;

Los nuevos modos de maximización y redimensionamiento del tamaño tienen que interactuar con los modos estándar (tamaño predefinido y minimización de la ventana de diálogo). Así, si la ventana está maximizada, no no se puede arrastrarla por el encabezado, lo que está permitido en el tamaño estándar. Además, el estado del botón de minimizar tiene que redefinirse al maximizar la ventana. Para este propósito necesitamos el acceso a las variables CEdit m_caption en la clase CDialog y CBmpButton m_button_minmax en la clase CAppDialog. Lamentablemente, ellos y muchos otros miembros de estas clases están declarados en la sección private. Eso parece bastante extraño, dado que estas clases básicos forman parte de la biblioteca pública que sirve para un amplio uso y expansión. Lo mejor sería declarar todos los miembros como protected, o por lo menos, tener los métodos para acceder a ellos. En este caso, no es así y no tenemos otra opción sino corregir la biblioteca estándar aplicando un pequeño patch. Naturalmente, eso supone un problema puesto que después de actualizar la biblioteca, habrá que aplicar el patch nuevamente, pero la única alternativa —crear las clases duplicadas CDialog y CAppDialog— no parece correcta desde el punto de vista de la ideología de la POO.

No es el último caso cuando la declaración de los miembros en las clases base como «privados» impide la expansión de la funcionalidad en las clases derivadas. A este respecto, se propone hacer una copia de la carpeta Include/Controls, y en caso de los errores de compilación "private member access error" editar zonas problemáticas: transferir el elemento correspondiente en la sección protected o simplemente sustituir private por protected.

Necesitamos reescribir algunos métodos virtuales de las clases 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;

Los tres primeros están relacionados con el botón de minimizar, los tres últimos, con el proceso del cambio del tamaño que se basa en la tecnología drag'n'drop.

También cubriremos los métodos virtuales para la creación del diálogo y reacción a los eventos (lo último siempre se usa de forma implícita en macrodefiniciones del mapa del procesamiento de eventos, que será considerado más tarde).

    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;

El botón de maximizar será creado al mismo tiempo con el botón estándar de minimizar en la versión redefinida CreateButtonMinMax. En este punto, está claro que se invoca primero la implementación base para obtener los botones estándar del encabezado, y luego, se dibuja el nuevo botón de maximizar a su lado. El código fuente representa un conjunto habitual para este caso de las instrucciones para definir las coordenadas iniciales, alineación, conexión de los recursos de imágenes, y por tanto, no va a mostrarse aquí. El código completo se adjunta al artículo. Los recursos para ambos botones se ubican en el subdirectorio "res":

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

El método para procesar los clics en el botón de maximizar es el siguiente:

    virtual void OnClickButtonTrueMax(void);

Además, añadimos los métodos auxiliares para maximizar la ventana y recuperar su tamaño inicial; ellos realizan prácticamente todo el trabajo, siendo invocados desde OnClickButtonTrueMax, dependiendo de que si la ventana está maximizada o no.

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

La creación del botón de redimencionamiento y el inicio del proceso de escalamiento se realiza en los siguientes métodos:

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

El procesamiento de eventos está determinado por las macros conocidas:

  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)

Los objetos m_button_truemax y m_button_size han sido creados por nosotros, mientras que m_drag_object ha sido heredado de la clase CWnd. Ahí, él se usa para mover la ventana por el encabezado. En nuestra clase, el mismo objeto va a participar en el cambio del tamaño de la ventana.

Pero todavía no es todo el trabajo con los eventos. Para interceptar los cambios del tamaño del gráfico, tenemos que procesar el evento CHARTEVENT_CHART_CHANGE. Para este propósito, describimos el método ChartEvent en nuestra clase: él va a solapar el métoddo similar en CAppDialog, y necesitaremos llamar a la implementación base. No obstante, aparte de eso, vamos a verificar el código del evento y ejecutar un procesamiento 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);
  }

El método OnChartChange rastrea el tamaño del gráfico, y si éste ha sido alterado en el modo activo de maximización, inicia un nuevo layout de los controles, eso se consigue llamando a un 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;
  }

En la clase MaximizableAppDialog, este método se define como virtual abstracto, es decir, la clase heredada tiene que ajustar sus controles teniendo en cuenta nuevo tamaño.

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

El mismo método se invoca desde otras zonas de la clase de la ventana de «goma», donde ocurre el redimensionamiento, por ejemplo, desde OnDialogDragProcess (cuando el usuario arrastra la esquina inferior derecha) y OnDialogDragEnd (cuando el usuario ha terminado el escalado).

El comportamiento del diálogo mejorado es el siguiente: después de mostrarlo con el tamaño estándar en el gráfico, el usuario puede moverlo a través del encabezado (comportamiento estándar), minimizarlo (comportamiento estándar) y maximizarlo (comportamiento adicional). El estado maximizado se mantiene al redimencionar el gráfico. Usted puede volver la ventana al estado inicial o minimizarla desde el estado maximizado usando el mismo botón. También se puede maximizar inmediatamente la ventana desde el estado minimizado. Si la ventana no está minimizada ni maximizada, en la esquina inferior derecha se muestra el área activa (botón triangular) para un escalamiento aleatorio. Si la ventana está minimizada o maximizada, esta área se desactiva y se oculta.

Aquí, podríamos terminar la implementación de MaximizableAppDialog. No obstante, durante el proceso del testeo, fue detectado otro detalle que requería el desarrollo adicional.

Resulta que en el estado minimizado de la ventana, el área activa del cambio del tamaño sobrepone el botón del cierre de la misma e intercepta los eventos del ratón. Es un error obvio de la biblioteca porque, en el estado minimizado, el botón de redimencionamiento se oculta y se desactiva. El problema se encuentra en el método CWnd::OnMouseEvent. Le falta esta verificación:

  // if(!IS_ENABLED || !IS_VISIBLE) return false; - falta esta línea

Como resultado, incluso los controles desactivados e invisibles interceptan los eventos. Obviamente, se podría solucionar el problema definiendo el orden Z apropiado de los controles. No obstante, aquí también se descubrió un fallo, es decir, no se toma en cuenta el orden Z de los controles. En particular, si echamos un vistazo al método CWndContainer::OnMouseEvent, veremos un ciclo simple en orden inverso por todos los controles subordinados, sin intentar determinar su prioridad en el orden Z.

Por esta razón, se requiere un patch adicional de la biblioteca o algún «truque» en la clase derivada. En este caso, fue elegida la segunda opción. El truque consiste en lo siguiente: en el estado minimizado, interpretamos el clic en el botón de redimensionamiento como el cierre de la ventana (ya que se sobrepone precisamente este botón). Para este propósito, el siguiente método fue adicionado a la clase MaximizableAppDialog:

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

Colocamos en el mapa de los eventos:

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

Ahora, la clase MaximizableAppDialog está realmente lista para el trabajo. Obsérvese que está destinado para el área principal del gráfico.

Primero, vamos a intentar entegrarla en el juego del 15 (taken). Después de copiar SlidingPuzzle2.mq5 y SlidingPuzzle2.mqh a los archivos con los números siguientes SlidingPuzzle3.mq5 y SlidingPuzzle3.mqh, empezamos a editarlos. Prácticamente, no es necesario modificar el archivo mq5, sólo basta con sustituir la referencia del archivo de encabezado incluido por SlidingPuzzle3.mqh.

En el archivo SlidingPuzzle3.mqh, sustituimos la conexión de la clase del diálogo estándar por el recién creado, es decir, en vez de:

  #include <Controls\Dialog.mqh>

colocamos:

  #include <Layouts\MaximizableAppDialog.mqh>

La descripción de la clase ahora debe usar una clase nueva padre:

  class CSlidingPuzzleDialog: public MaximizableAppDialog // CAppDialog

Además de eso, la sustitución semejante de los nombres debe realizarse en el mapa de los eventos:

  EVENT_MAP_END(MaximizableAppDialog) // CAppDialog

Y también en el 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, el nuevo diálogo requiere la implementación del método SelfAdjustment que responde al cambio del tamaño.

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

Aquí, delegamos todo el trabajo al contenedor m_main llamando a su método Pack para el último tamaño conocido del área de cliente de la ventana.

Eso es absolutamente suficiente para que el juego adquiera un layout ataptable. No obstante, para mejorar la legibilidad y eficiencia del código, alteré un poco el concepto del trabajo con los botones en la aplización: ahora ellos están combinados en un array único CButton m_buttons[16], están disponibles por el índice, en vez de usar el operador switch, y se procesan a través de la única cadena (con el método OnClickButton) en el mapa de los eventos:

  ON_INDEXED_EVENT(ON_CLICK, m_buttons, OnClickButton)

Los interesados pueden comparar los códigos fuente del juego original y del juego modificado.

Así se comporta la ventana adaptable en el gráfico.

Taken

Taken

La adaptación del EA de demostración Experts\Examples\Layouts\Controls2.mq5 se realiza de la misma manera: su archivo principal mq5 y el archivo de cabecera incluido con la descripción del cuadro de diálogo están representados con nuevos nombres: Controls3.mq5 y ControlsDialog3.mqh. Cabe destacar que el juego usa el contenedor tipo tabla (grid), mientras que el cuadro de diálogo con los controles se base en los paneles (box).

Si en el proyecto modificado, dejamos la misma implementación del método SelfAdjustment como en el juego, es fácil de notar un fallo que se ha escapado de nuestra atención hasta ahora: el redimencionamiento adaptable funciona por ahora en la propia ventana, y no afecta los controles, pero sería bueno poder ajustarlos también dinámicamente al tamaño de la ventana.

Controles de «goma»

Los controles estándar se adaptan al cambio dinámico de su tamaño de manera diferente. Algunos controles, como los botones CButton, responden correctamente a las llamadas del método Width. Para algunos, como las listas CListView, es suficiente establecer el alineamiento usando Alignment, y el sistema va a mantener automáticamente las lagunas entre el control y el borde de la ventana, lo que equivale a la «elasticidad». Sin embargo, algunos controles no soportan ninguno de los métodos. Entre ellos están, por ejemplo, CSpinEdit o CComboBox. Para dotarlos con esa nueva habilidad, necesitamos crear unas subclases.

En caso de CSpinEdit, basta con sobrescribir el 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();
      }
  };

Puesto que CSpinEdit se compone prácticamente de 3 controles (campo de entrada y dos botones de «columpio»), a la respuesta de la solicitud de cambiar el tamaño (que es lo que hace el método OnResize) estiramos o comprimimos el campo de entrada hasta un valor nuevo, y movemos los botones al borde derecho del campo. El único problema es que los elementos subordinados (m_edit, m_inc, m_dec) se describen en la sección private. Así, nos enfrentamos nuevamente con la necesidad de corregir la biblioteca estándar. Aunque CSpinEdit era necesario sólo para demostrar el enfoque que, en este caso, se implementa de una forma bastante simple. Mientras que para una interfaz OLAP real, necesitamos una lista desplegable adaptada.

Pero el problema similar surge en el caso de la personalización de la case CComboBox. Antes de implementar la clase derivada, es necesario aplicar un patch a la CComboBox base, sustituyendo private por protected. Nótese que todos estos «patches» no afectan la compatibilidad con otros proyectos que usan la biblioteca estándar.

La implementación de la «elasticidad» del cuadro combinado (combobox) requiere un poco más de esfuerzo. Aquí, hace falta sustituir no sólo OnResize, sino también OnClickButton, Enable, Disable, e incluso añadir el mapa de eventos. Tenemos que controlar los objetos subordinados m_edit, m_list, m_drop, es decir, todo de lo que se compone el combobox.

  #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)

Después de obtener los controles «elásticos», podemos verificarlos en el proyecto de demostración Controls3. Sustituimos las clases CSpinEdit y CComboBox por SpinEditResizable y ComboBoxResizable, respectivamente. En el método SelfAdjustment, alteramos los tamaños de los 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();
  }

El método SelfAdjustment va a invocarse por la clase padre MaximizableAppDialog después de redimencionar la ventana automáticamente. Además, nosotros mismos llamaremos a este método una vez en el momento de la inicialización de la ventana desde el método CreateMain.

En realidad, eso puede ser algo como esto (aquí, para simplificar, los controles ocupan el área de trabajo de la ventana, pero también se puede aplicar el mismo efecto por la vertical).

Demostración de los controles

Demostración de los controles

Los bordes rojos se muestran con fines de la depuración, pudiendo desactivarlos usando la macro LAYOUT_BOX_DEBUG.

Aparte de las modificaciones mencionadas, también he modificado ligeramente el principio de la inicialización de los controles. Cada bloque, empezando del área de cliente principal de la ventana, se inicializa totalmente en el método seleccionado (por ejemplo, CreateMain, CreateEditRow, CreateButtonRow, etc), que devuelve una referencia al contenedor creado tipo (CWnd *) en caso del éxito. El contenedor padre añade el subordinado llamando a CWndContainer::Add. Es el método principal de la inicialización del cuadro de 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;
  }

Es la inicialización de una línea con botones:

  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;
  }

Esta sintaxis parece más lógica y compacta en comparación con la de antes, pero puede dificultar la comparación contextual de los proyectos antiguos y nuevos, téngalo presente.

Lamentablemente, nuestra «epopeya» con los controles no termina aquí. Recordaré que el objetivo del proyecto es hacer una interfaz gráfica para OLAP. Por eso, el control «gráfico» debe ocupar el lugar central. El problema es que este control simplemente no existe en la biblioteca estándar. Tendremos que crearlo.

Control «gráfico» (CPlot)

La biblioteca MQL ofrece varias primitivas gráficas. Entre ellas hay lienzo para dibujado (CCanvas), gráfico comercial a base del lienzo (CGraphic) y, finalmente, objetos gráficos para mostrar las imágenes ya hechas (CChartObjectBitmap, CPicture) —lamentablemente, ellos no están relacionados de ninguna manera con el gráfico comercial. Para insertar una de las primitivas mencionadas en la interfaz de la ventana, es necesario envolverla en la clase heredera del control, que es capaz de dibujar. Afortunadamente, no es necesario resolver este problema desde cero. Por favor, consulte el artículo Gráfico PairPlot basado en CGraphic para analizar correlaciones entre los arrays de datos (series temporales) . En este artículo, se ofrece una clase del control hecha que incluye un conjunto de gráficos para analizar las dependencias pareadas entre varios símbolos. De esta manera, basta con modificarla para trabajar con un único gráfico en el control, y así obtener el resultado deseado.

Los códigos fuente del artículo se colocan en el directorio Include\PairPlot\. El archivo contiene la clase que nos interesa PairPlot.mqh. Basándose en él, creamos nuestra propia versión, con el nombre Plot.mqh. Las diferencias principales serán las siguientes.

No necesitamos la clase CTimeserie, por eso, vamos a eliminarla. La clase CPairPlot, que es un control derivado de CWndClient, la transformamos en CPlot, sustituyendo en ella todo el trabajo con el array de los gráficos de los pares de moneda por un único gráfico. Los gráficos de aquel proyecto se dibujan a través de unas clases especiales del histograma (CHistogram) y del diagrama de dispersión (CScatter), que son derivadas de la clase base común CPlotBase, que a su vez es un heredero de CGraphic. Transformamos CPlotBase en nuestra propia clase CGraphicInPlot, también heredada de CGraphic, mientras que los histogramas especiales y los diagramas de dispersión no serán necesarias. En vez de ellos, vamos a usar los estilos estándar del dibujado (CURVE_POINTS, CURVE_LINES, CURVE_POINTS_AND_LINES, CURVE_STEPS, CURVE_HISTOGRAM), proporcionados por la clase CGraphic, para ser más exactos, por su clase subordinada CCurve. El diagrama simplificada de las relaciones de las clases se muestra más abajo.

Diagrama de las relaciones de las clases gráficas

Diagrama de las relaciones de las clases gráficas

Las clases añadidas están marcadas en gris, las demás son estándares.

Para testear el trabajo del nuevo control, creamos el EA de prueba PlotDemo. Como siempre, la inicialización, vinculación a los eventos y el inicio se realizan en el archivo PlotDemo.mq5, y la descripción del diálogo se hace en PlotDemo.mqh (ambos archivos se adjuntan).

El EA recibe el único parámetro de entrada, o sea, el estilo del dibujado.

  #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);
  }
  
  ...

En el archivo de cabecera del dialogo, creamos el objeto de nuestro control y añadimos dos curvas de prueba.

  #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();
  }

En la animación de abajo, se puede ver cómo trabaja este EA:

Demostración del control con la gráfica comercial

Demostración del control con la gráfica comercial

Pues bien, hemos hecho mucho trabajo y ahora las posibilidades de construir una interfaz adaptable con soporte de la gráfica corresponden a nuestras exigencias dentro del proyecto OLAP. Para resumir, presentamos el diagrama de las clases principales asociadas a la interfaz gráfica del usuario.

Diagrama de las clases de los controles

Diagrama de las clases de los controles

El color blanco marca las clases estándar, el amarillo marca las clases de los contenedores, el rosado marca las clases de los diálogos y los elementos personalizados que soportan el redimencionamiento, el verde marca el control con la gráfica comercial incorporada.

Interfaz gráfica para OLAP

El nuevo experto OLAPGUI se encargará del procesamiento interactivo de datos del historial comercial y su visualización. Todas las operaciones relacionadas con la creación de los controles, la reacción a las acciones del usuario y llamadas a las funciones OLAP se contienen en el archivo de cabecera OLAPGUI.mqh.

En las variables de entrada del EA, dejamos sólo lo que está relacionado con la importación de datos desde HTML o CSV. En primer lugar, eso se refiere a las variables ReportFile, Prefix, Suffix, las que ya conocemos desde el primer proyecto OLAPDEMO. Si ReportFile se deja vacío, va a analizarse el historial de trading de la cuenta actual.

Usaremos los controles para seleccionar los selectores, agregadores y el estilo del gráfico. Como antes, dejamos la posibilidad de especificar 3 dimensiones del hipercubo, es decir, 3 selectores por los ejes condicionales X, Y, Z. Para eso, necesitamos 3 listas desplegables. Vamos a colocarlas en la primera línea superior de los controles. En la misma línea, del lado derecho, hacemos el botón Process, cuya pulsación inicia el análisis.

La selección de la función del agregador y de los campos para la agregación está implementada con otras dos listas desplegables (en la segunda línea de los controles). Ahí mismo, añadimos una lista desplegable para el orden de la clasificación y el estilo del gráfico. Excluimos la filtración del proyecto para simplificar la interfaz.

El gráfico ocupara todo el espacio restante de la ventana.

Las listas desplegables con selectores van a contener el mismo conjunto de opciones. Dentro de él, combinaremos los tipos de los selectores y los campos de entradas mostrados directamente. En la siguiente tabla, se muestran los nombre de los elementos y los campos y/o tipos de selectores que les corresponden.

La selección de los selectores marcados con * se determina por el tipo del agregador: en caso de IdentityAggregator se usa TradeSelector, de lo contrario, QuantizationSelector.

Los nombres de los selectores (puntos de 1 a 9) en la lista desplegable están encerrados entre comillas.

Los selectores tienen que seleccionarse consecutivamente de izquierda a derecha, de X a Z. Hasta que no esté seleccionado un selector de la medición anterior, los cuadros combinados (combobox) para los ejes subsiguientes están ocultados.

Funciones agregadas soportadas

Todos, excepto el último, exigen la especificación del campo de entrada a ser agregado usando la lista desplegable a la derecha del agregador.

La función "progressive total" implica que el selector en el eje X tiene seleccionado "ordinal" (repaso consecutivo de las entradas).

El combobox con ordenación está disponible sólo si está seleccionado el único selector (X).

Los ejes X y Y se ubican en el gráfico por la horizontal y por la vertical, respectivamente. Para los hipercubos tridimensionales con diferentes coordenadas por el eje Z, fue aplicado un enfoque más primitivo posible: usando el botón Progress, se puede repasar varias secciones transversales en el plano Z. Si hay coordenadas Z, el botón cambia el nombre por "i / n title >>", donde i es el número de la coordenada Z, n es el número total de muestras por el eje Z, title nombre de la muestra (por ejemplo, el día de la semana o el tipo de transacciones, dependiendo del selector en el eje Z). Si cambiamos las condiciones de la construcción del hipercubo, el botón recibe de nuevo el encabezado "Process" y empieza a trabajar en modo normal. Nótese que durante el trabajo del agregador "identity", el procesamiento es diferente: en este caso, el cubo siempre tiene 2 dimensiones, y las tres curvas (para los campos X, Y, Z) se muestran en el gráfico juntas, sin scrolling.

Cada cubo no sólo se muestra gráficamente, sino también se muestra en el log como texto. Eso es especialmente importante para los casos cuando la agregación se realiza en campos simples, en vez de los selectores. Los selectores garantizan la visualización de las marcas por los ejes, mientras que al cuantificar un campo simple, el sistema puede mostrar sólo el índice de la célula. Por ejemplo, si queremos analizar el beneficio en división por los tamaños de lotes, podemos seleccionar el campo "lot" en el selector X y el agregador "sum" en el campo "profit amount". En este caso, las muestras para 0, 0.5, 1, 1.0, 1.5, etc. pueden aparecer en el eje X hasta el número de diferentes volúmenes con los que se realizaba la negociación. Sin embargo, serán precisamente los números de las células, en vez de los valores de lotes (se puede ver los últimos en el log). Habrá aproximadamente lo siguiente:

	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"

Aquí, value es el beneficio total, title es el tamaño real del lote correspondiente a este beneficio, los números a la izquierda son las coordenadas en el eje X. Obsérvese que en el gráfico, a lo largo del eje, también pueden mostrarse los valores fraccionados, a pesar de que el sentido tienen sólo los índices enteros. Este y otros aspectos de la muestra de las marcas, desde luego, pueden ser mejorados.

Para vincular los controles de la interfaz gráfica con el núcleo de OLAP, (que dejamos inalterado como en el primer artículo en el archivo de cabecera OLAPcube.mqh) necesitamos implementar la clase intermedia OLAPWrapper. Realmente, ella realizará el mismo trabajo preparatorio con los datos hecho por la función process en el primer proyecto de demostración OLAPDEMO, pero ahora será el método de la clase.

  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);
        ...
      }

El código completo se adjunta al artículo. Obsérvese que todos los ajustes que recibíamos desde las variables de entrada en el proyecto OLAPDEMO, ahora se transfieren como parámetros del método process y ellos, obviamente, tienen que rellenarse a base del estado de los controles.

El parámetro display es de interés particular. El núcleo OLAP declara esta interfaz especial Display para visualizar los datos, y necesitamos implementarla en la parte gráfica del programa. Después de crear un objeto con esta interfaz, vamos a implementar la «inyección de dependencias» (en inglés, dependency injection), que fue discutida en el artículo anterior. Eso permite ejecutar la conexión de un nuevo método de visualización de los resultados sin modificar el núcleo de OLAP.

En el archivo OLAPGUI.mq5, creamos el diálogo, pasándole una instancia de OLAPWrapper.

  #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;
  }
  ...

La clase del diálogo OLAPDialog está definida en 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);
      ...

En respuesta a la pulsación del botón "Process", el diálogo, a base de la posición de los controles, rellena los parámetros necesarios para el método OLAPWrapper::process y lo invoca, pasando el objeto olapdisplay como un display:

   virtual voidHide(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);
  }

El código completo del ajuste de todos los parámetros se adjunta.

Necesitamos la clase auxiliar OLAPDisplay porque MQL no soporta la herencia múltiple. La clase OLAPDialog se deriva de MaximizableAppDialog, por eso, no puede implementar directamente la interfaz Dialog. En vez de eso, delegamos esta tarea a la clase OLAPDisplay, creamos su objeto dentro de la ventana y proveemos con una referencia al creador por medios del parámetro del constructor.

Después de construir el cubo, el núcleo OLAP llama al 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;
    }
    ...

La esencia de lo sucedido consiste en obtener los datos (getDimension(), getDimensionTitle(), getVector()) del objeto metaData, y pasarlos a la ventana. En el fragmento de arriba, se observa la versión del procesamiento del caso con el único selector. Los métodos especiales para recibir los datos están reservados en la clase de nuestro diálogo:

  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");
  }

Estos son ejemplos de algunas secciones analíticas que pueden ser recibidas gráficamente usando OLAPGUI.

Beneficios por los símbolos en orden descendiente

Beneficios por los símbolos en orden descendiente

Beneficios por los símbolos en orden alfabético

Beneficios por los símbolos en orden alfabético

Beneficios por los símbolos, día de la semana, tipo «compra»

Beneficios por los símbolos, día de la semana, tipo «compra»

Beneficios por los símbolos, día de la semana, tipo «venta»

Beneficios por los símbolos, día de la semana, tipo «venta»

Beneficios por el tamaño del lote (los lotes están especificados como índices de las células, los valores se muestran en el log)

Beneficios por el tamaño del lote (los lotes están especificados como índices de las células, los valores se muestran en el log)

Curva del saldo total

Curva del saldo total

Balance por compras y ventas

Balance por compras y ventas

Curva del balance para cada símbolo por separado

Curva del balance para cada símbolo por separado

Curvas de los swaps para cada símbolo por separado

Curvas de los swaps para cada símbolo por separado

Dependencia de los beneficios de la duración del trade para cada símbolo por separado

Dependencia de los beneficios de la duración del trade para cada símbolo por separado

Número de transacciones por símbolos y por tipos

Número de transacciones por símbolos y por tipos

Dependencia de los campos y la duración para cada transacción (duración expresada en segundos)

Dependencia de los campos y la duración para cada transacción (duración expresada en segundos)

Dependencia MFE (%) y MAE (%) para todas las transacciones

Dependencia MFE (%) y MAE (%) para todas las transacciones

Lamentablemente, el estilo estándar del dibujado del histograma no supone la visualización de varios arrays con el desplazamiento de las columnas del mismo índice desde arrays diferentes. En otras palabras, los valores con la misma coordenada pueden sobreponerse completamente. Para resolver este problema, es necesario implementar su propio método de visualizar el histograma (lo que permite crear la clase), pero eso se deja para el trabajo individual.

Conclusiones

En este artículo, hemos considerado los principios generales del desarrollo de la interfaz gráfica personalizada para los programas MQL que soportan el redimencionamiento y la disposición universal de los controles. Basándose en la tecnología, fue creada la aplicación interactiva para analizar los informes comerciales, usando los recursos del primer artículo desde la serie OLAP. La visualización de diferentes indicadores en combinaciones y secciones aleatorias permite revelar las regularidades y simplifica el análisis de criterios múltiples con el fin de optimizar de sistemas comerciales.

A continuación, se muestran las listas de los archivos adjuntos de los proyectos.

Proyecto OLAPGUI

Proyecto SlidingPuzzle3

Проект Controls3

Proyecto PlotDemo