Применение OLAP в трейдинге (Часть 2): Визуализация результатов интерактивного анализа многомерных данных

14 мая 2019, 08:59
Stanislav Korotky
9
2 414

В первой статье, посвященной OLAP в трейдинге, мы рассмотрели общие принципы аналитической обработки многомерных данных и предоставили готовые классы MQL, позволяющие применить OLAP на практике для истории счета или торговых отчетов. Вместе с тем вывод результатов анализа был пока реализован в упрощенном текстовом виде в лог экспертов. Для улучшения наглядности требуется создать новый класс, наследник интерфейса Display, способный визуализировать данные OLAP с помощью деловой графики. Как оказалось, данная задача требует большой подготовительной работы и затрагивает много различных аспектов, никак не связанных с OLAP. Так что давайте на время отложим обработку данных в сторону и постепенно, шаг за шагом, проработаем графический интерфейс MQL-программы.

Для реализации графического интерфейса на MQL существует несколько библиотек, включая Стандартную библиотеку управляющих элементов (Include/Controls). Одно из заметных упущений практически во всех библиотеках связано с тем, что не существует средств автоматического управления раскладкой элементов в окне. Иными словами, позиционирование и выравнивание элементов производится статически с помощью жестко зашитых в код констант с координатами X и Y. С этой проблемой тесно связана и другая — отсутствие средств визуального дизайна экранных форм. Это еще более сложная, хотя и выполнимая задача. Но поскольку в данном проекте интерфейс был все же не главной темой, было решено на редактор экранных форм не отвлекаться и сосредоточится на более простом подходе с реализацией адаптивного интерфейса. В таком интерфейсе элементы должны быть особым образом организованы в группы, которые поддерживали бы взаимное расположение и правила масштабирования автоматически.

Здесь мы сталкиваемся с еще одним нюансом Стандартной библиотеки: её диалоговые окна имеют фиксированный размер. Однако при визуализации больших гиперкубов OLAP пользователю было бы удобно иметь возможность максимизировать окно или хотя бы растянуть его достаточно, чтобы метки ячеек уместились на осях без наложений.

На сайте mql5.com есть единичные открытые наработки в области графических интерфейсов, которые решают некоторые из перечисленных проблем, но соотношение показателей сложность/возможности у них далеки от компромиссных: либо возможности ограничены (например, есть механизм раскладки элементов, но нет масштабирования), либо интеграция требует больших усилий (на чтение пространной документации, освоение нестандартных способов работы и т.д.). Кроме того, при прочих равных условиях предпочтительнее использовать решение на базе стандартных элементов, как более устоявшихся и широко распространенных (читай — задействованных в большем числе MQL-программ и потому с большим коэффициентом полезности).

В результате я выбрал в качестве отправной точки взвешенное, на мой взгляд, — простое и технологичное решение, предложенное в статьях Применение контейнеров для компоновки графического интерфейса: класс CBox и Применение контейнеров для компоновки графического интерфейса: класс CGrid Энрико Ламбино.

В первой статье элементы управления помещаются в контейнеры с горизонтальной или вертикальной раскладкой, причем они могут быть вложенными, и этим достигается совершенно произвольная планировка интерфейса. Во второй статье предложены контейнеры с табличной организацией. И первые, и вторые работают со всеми стандартными элементами управления и будут работать с прочими корректно написанными "контролами" на базе CWnd.

Единственное, чего здесь не хватает, это динамического изменения размера окна и контейнеров. Это и будет первым шагом в решении общей задачи.

"Резиновые" окна

Классы CBox и CGrid подключаются к проектам в виде заголовочных файлов Box.mqh, Grid.mqh, GridTk.mqh. Если Вы используете архивы из статей, то данные файлы должны устанавливаться в каталог Include/Layouts.

Внимание! В Стандартной библиотеке уже есть структура CGrid. Она предназначена для отрисовки сетки на графиках. Класс контейнера CGrid не имеет к этому никакого отношения. Совпадение имен — неприятно, но не критично.

Мы исправим в файле GridTk.mqh маленькую ошибку и слегка дополним файл Box.mqh, после чего можно будет приступать непосредственно к усовершенствованию стандартного класса диалогов — CAppDialog. Разумеется, мы не будем ломать имеющийся класс, а создадим новый, производный от CAppDialog.

Основная часть изменений приходится на метод CBox::GetTotalControlsSize (соответствующие строки помечены комментариями). Желающие могут сравнить файлы оригинальных проектов с файлами, приложенными к данной статье.

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

Если вкратце, то модифицированная версия учитывает возможное динамическое изменение размера элементов.

В оригинальных статьях в качестве тестовых примеров использовались эксперты Controls2 (аналог стандартного проекта Controls, поставляемого вместе с MetaTrader в папке Experts\Examples\Controls\) и игра пятнашки — SlidingPuzzle2. Оба примера контейнеров располагаются по умолчанию в папке Experts\Examples\Layouts\. Именно на их основе попробуем реализовать и протестировать "резиновые" окна.

Создадим файл MaximizableAppDialog.mqh в Include\Layouts\. Класс окна унаследуем от CAppDialog

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

Нам потребуются 2 новых кнопки с картинками: одна для максимизации окна (она будет расположена в заголовке, рядом с кнопкой минимизации), а вторая для свободного изменения размера — в правом нижнем углу.

  protected:
    CBmpButton m_button_truemax;
    CBmpButton m_button_size;

Признак текущего максимизированного состояния или процесса изменения размера будем хранить в соответствующих логических переменных.

    bool m_maximized;
    bool m_sizing;

Также мы добавим прямоугольник, в котором будем постоянно динамически отслеживать размер графика (чарта) на случай, если окно максимизировано (и его нужно подстроить при изменении чарта), а также некий минимальный размер, меньше которого окно делать нельзя (пользователь сможет установить такое ограничение с помощью публичного метода SetSizeLimit).

    CRect m_max_rect;
    CSize m_size_limit;

Новые добавляемые режимы максимизации и изменения размера должны взаимодействовать со стандартными режимами — размера по умолчанию и минимизации диалога. Так, если окно максимизировано, оно не должно перетаскиваться за заголовок, что разрешено при стандартном размере. Также состояние кнопки минимизации должно сбрасываться при максимизации окна. Для этих целей нам необходим доступ к переменным CEdit m_caption в классе CDialog и CBmpButton m_button_minmax в классе CAppDialog. К сожалению, они, да и многие другие члены этих классов, объявлены в секции private. Это выглядит довольно странным, учитывая, что данные базовые классы входят в состав публичной библиотеки, предназначенной для широкого использования и расширения. По-хорошему все члены должны были бы быть объявлены protected или хотя бы иметь методы для доступа к ним. В данном случае это не так, и у нас не остается другого выхода, кроме как исправить стандартную библиотеку, применив небольшой "патч". Это, конечно, представляет проблему, поскольку после обновления библиотеки "патч" нужно будет применять снова, но и единственная альтернатива — создать классы-дубликаты CDialog и CAppDialog — не представляется верной с точки зрения идеологии ООП.

Это — не последний случай, когда объявление членов в базовых классах "приватными" будет препятствовать расширению функционала в производных классах. В связи с этим предлагается сделать копию папки Include/Controls и при возникновении ошибок компиляции "private member access error" редактировать проблемные участки: переносить соответствующий элемент в секцию protected или просто заменять private на protected.

Нам потребуется переписать некоторые виртуальные методы базовых классов:

    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;

Первые три связаны с кнопкой минимизации, три последних — с процессом смены размера, который основывается на технологии drag'n'drop.

Также будут перекрыты виртуальные методы создания диалога и реакции на события (последнее всегда неявным образом используется в макроопределениях карты обработки событий, которую мы рассмотрим чуть позже).

    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;

Кнопка максимизации будет создаваться заодно со стандартной кнопкой минимизации в переопределенной версии CreateButtonMinMax. Там, разумеется, сперва вызывается базовая реализация, чтобы получить стандартные кнопки заголовка, а затем рядом "подрисовывается" новая кнопка максимизации. Исходный код представляет собой обычный для такого случая набор инструкций по установке начальных координат, выравнивания, подключения ресурсов изображений, и потому здесь приводиться не будет. С полным исходным кодом можно ознакомиться в приложении. Ресурсы для обеих кнопок расположены в подкаталоге "res":

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

За обработку нажатий на кнопку максимизации отвечает метод:

    virtual void OnClickButtonTrueMax(void);

Кроме того мы добавим вспомогательные методы для максимизации окна на весь чарт и восстановления его в исходный размер — они, фактически, выполняют всю работу, будучи вызваны из OnClickButtonTrueMax в зависимости от того, максимизировано ли окно или нет.

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

Создание кнопки изменения размера и запуск процесса масштабирования реализовано в следующих методах:

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

Обработка событий определяется привычными макросами:

  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)

Объекты m_button_truemax и m_button_size мы создали сами, а вот m_drag_object унаследован от класса CWnd. Там он используется для перемещения окна за заголовок. В нашем классе тот же объект будет участвовать в изменении размера окна.

Но это еще не вся работа с событиями. Для того чтобы перехватывать изменения размера чарта, нам нужно обрабатывать событие CHARTEVENT_CHART_CHANGE. Для этой цели опишем в своем классе метод ChartEvent — он будет перекрывать аналогичный метод в CAppDialog, и нам потребуется вызвать базовую реализацию. Однако помимо этого будем проверять код события и для 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);
  }

Метод OnChartChange отслеживает размер чарта, и, если он изменен при активном режиме максимизации, то инициирует новую раскладку элементов — это достигается вызовом специального метода 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;
  }

В классе MaximizableAppDialog этот метод определен как абстрактный виртуальный, то есть класс-наследник должен будет позаботиться о приведении своих "контролов" в порядок для нового размера.

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

Этот же метод вызывается из других мест класса "резинового" окна, где происходит изменение размера, например, из OnDialogDragProcess (когда пользователь перетаскивает нижний правый угол) и OnDialogDragEnd (когда пользователь завершил масштабирование).

Поведение усовершенствованного диалога такое: после того как он отображен в стандартном размере на чарте, пользователь может его перемещать за заголовок (стандартное поведение), минимизировать его (стандартное поведение) и максимизировать (добавленное поведение). Максимизированное состояние сохраняется при изменении размера чарта. Из максимизированного состояния той же кнопкой можно вернуть окно в исходный размер или минимизировать. Из минимизированного состояния также можно сразу максимизировать окно. Если окно не минимизировано и не максимизировано, в правом нижнем углу отображается активная область (треугольная кнопка) для произвольного масштабирования. Если окно минимизировано или максимизировано, эта область деактивируется и скрывается.

На этом реализация MaximizableAppDialog могла бы быть завершена. Однако в процессе тестирования был выявлен еще один нюанс, потребовавший доработки.

Дело в том, что в минимизированном состоянии окна активная область изменения размера перекрывает кнопку закрытия окна и перехватывает от неё события мыши. Это очевидная ошибка библиотеки, поскольку в минимизированном состоянии кнопка "ресайза" скрывается и делается неактивной. Проблема находится в методе CWnd::OnMouseEvent. В нем не хватает такой проверки:

  // if(!IS_ENABLED || !IS_VISIBLE) return false; - этой строки нет

В результате даже отключенные и невидимые "контролы" перехватывают события. Очевидно, что проблему можно было бы решить с помощью установки соответствующего Z-порядка управляющих элементов. Однако и здесь обнаружилась недоработка — библиотека не учитывает Z-порядок элементов. В частности, если заглянуть в метод CWndContainer::OnMouseEvent, то мы увидим простой цикл в обратном порядке по всем подчиненным элементам, без попыток определить их приоритет по Z-порядку.

Таким образом, требуется либо очередной "патч" библиотеки, либо некий "финт" в производном классе. В данном случае был выбран второй вариант. "Трюк" заключается в том, чтобы в состоянии минимизации трактовать нажатия на кнопку изменения размера как на закрытие окна (поскольку перекрывается именно эта кнопка). Для этих целей в класс MaximizableAppDialog был добавлен метод:

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

и помещен в карту событий:

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

Теперь класс MaximizableAppDialog действительно готов к работе. Обратите внимание, что он предназначен для основной области чарта.

Для начала попробуем встроить его в игру пятнашки. Скопировав SlidingPuzzle2.mq5 и SlidingPuzzle2.mqh в файлы с очередными номерами SlidingPuzzle3.mq5 и SlidingPuzzle3.mqh, приступим к их редактированию. Файл mq5 практически не нужно менять, достаточно только заменить ссылку подключаемого заголовочного файла на SlidingPuzzle3.mqh.

В файле SlidingPuzzle3.mqh заменим подключение класса стандартного диалога на вновь созданный, то есть вместо:

  #include <Controls\Dialog.mqh>

вставим:

  #include <Layouts\MaximizableAppDialog.mqh>

Описание класса теперь должно использовать новый родительский класс:

  class CSlidingPuzzleDialog: public MaximizableAppDialog // CAppDialog

Кроме того, аналогичную замену имен классов нужно произвести в карте событий:

  EVENT_MAP_END(MaximizableAppDialog) // CAppDialog

И еще в одном месте — в методе 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);
    ...

Наконец, новый диалог требует реализации метода SelfAdjustment, реагирующего на изменение размера.

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

Здесь мы делегируем всю работу контейнеру m_main, вызывая его метод Pack для последнего известного размера клиентской области окна.

И этого абсолютно достаточно, чтобы игра приобрела адаптивную раскладку. Однако в целях улучшения читабельности и эффективности кода, я несколько изменил концепцию работы с кнопками в приложении: теперь они сведены в единый массив CButton m_buttons[16], доступны по индексу, а не с помощью оператора switch, и обрабатываются единственной строкой (методом OnClickButton) в карте событий:

  ON_INDEXED_EVENT(ON_CLICK, m_buttons, OnClickButton)

Желающие могут сравнить исходные коды оригинальной игры и модифицированной.

Вот как ведет себя адаптивное окно на графике.

Игра Пятнашки

Игра Пятнашки

Аналогичным образом производится адаптация демонстрационного эксперта Experts\Examples\Layouts\Controls2.mq5 — его основной mq5-файл и подключенный заголовочный файл с описанием диалога представлены под новыми именами: Controls3.mq5 и ControlsDialog3.mqh. Следует отметить, что игра использовала контейнер типа таблицы (grid), а диалог с элементами управления построен на основе панелей (box).

Если в измененном проекте оставить такую же реализацию метода SelfAdjustment, как в игре, то легко заметить недочет, оставшийся пока вне нашего внимания: адаптивное изменение размера работает пока только на самом окне, но не сказывается на элементах управления, а желательно уметь их тоже подстраивать динамически под размер окна.

"Резиновые" элементы управления

Стандартные элементы управления по-разному приспособлены к динамической смене своего размера. Некоторые, такие как кнопки CButton, корректно реагируют на вызов метода Width. Некоторым, таким как списки CListView, достаточно задать выравнивание с помощью Alignment, и система будет автоматически сохранять зазоры между "контролом" и границей окна, что эквивалентно "резиновости". Однако некоторые элементы не поддерживают ни один из способов. К их числу относятся, например, CSpinEdit или CComboBox. Для наделения их новой способностью потребуется создать подклассы.

В случае CSpinEdit достаточно перекрыть виртуальный метод 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();
      }
  };

Поскольку CSpinEdit состоит фактически из 3 элементов — поля ввода и двух кнопок-"качалок", в ответ на запрос об изменении размера (чем и занимается метод OnResize) мы растягиваем или сжимаем поле ввода до новой величины, а кнопки передвигаем к правому краю поля. Единственная проблема заключается в том, что подчиненные элементы — m_edit, m_inc, m_dec — описаны в разделе private. Таким образом, мы вновь сталкиваемся с необходимостью исправления стандартной библиотеки. Впрочем, CSpinEdit был нужен только для демонстрации подхода, который в данном случае реализуется очень просто. А для реального интерфейса OLAP нам потребуется адаптированный выпадающий список.

Но аналогичная проблема возникает и в случае кастомизации класса CComboBox. Прежде чем реализовывать производный класс, нужно применить "патч" к базовому CComboBox с заменой private на protected. Отметим, что все эти "патчи" не сказываются на совместимости с другими проектами, использующими стандартную библиотеку.

Реализация "резиновости" "комбобокса" требует чуть больших усилий. Здесь нужно переопределить не только OnResize, но и OnClickButton, Enable, Disable, и даже добавить карту событий. Управлять приходится подчиненными объектами m_edit, m_list, m_drop — то есть всем, из чего состоит "комбобокс".

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

После того, как у нас появились "резиновые" элементы управления, мы можем проверить их на демонстрационном проекте Controls3. Заменим классы CSpinEdit и CComboBox на SpinEditResizable и ComboBoxResizable соответственно. В методе SelfAdjustment изменим размеры "контролов".

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

Метод SelfAdjustment будет вызываться родительским классом MaximizableAppDialog после изменения размеров окна автоматически. Кроме того, мы сами вызовем этот метод единожды в момент инициализации окна, из метода CreateMain.

В действии это может выглядеть примерно так (здесь для простоты "контролы" заполняют рабочую область окна только по горизонтали, но тот же эффект можно применить и по вертикали).

Демонстрация элементов управления

Демонстрация элементов управления

Красные рамки выведены в целях отладки — они отключаются с помощью макроса LAYOUT_BOX_DEBUG.

Помимо вышеперечисленных правок я также слегка модифицировал принцип инициализации "контролов". Каждый блок, начиная с главной клиентской области окна, целиком инициализируется в выделенном методе (например, CreateMain, CreateEditRow, CreateButtonRow и т.д.), который в случае успеха возвращает ссылку на созданный контейнер типа (CWnd *). Родительский контейнер добавляет подчиненный с помощью вызова CWndContainer::Add. Вот как теперь выглядит главный метод инициализации диалога:

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

А вот как выглядит инициализации ряда с кнопками:

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

Данный синтаксис кажется более логичным и компактным по сравнению с использовавшимся ранее, но может затруднить контекстное сравнение старых и новых проектов — имейте это в виду.

На этом наша "эпопея" с элементами управления, к сожалению, не заканчивается. Напомню, что цель проекта — обеспечить графический интерфейс для OLAP. Поэтому центральное место в нем должен занимать элемент управления "график". И проблема в том, что такого элемента в стандартной библиотеке просто нет. Его придется создать.

Элемент управления "график" (CPlot)

Библиотека MQL предоставляет несколько графических примитивов. В их числе — холст для рисования (CCanvas), деловая графика на основе холста (CGraphic) и, наконец, графические объекты для отображения уже готовых изображений (CChartObjectBitmap, CPicture) — к сожалению, они никак не связаны с деловой графикой. Нам для встраивания в интерфейс окна требуется обернуть что-либо из вышеперечисленного в класс-наследник элемента управления, способный рисовать. К счастью, эту задачу не нужно решать с нуля. На сайте опубликована статья График PairPlot на основе CGraphic для анализа зависимостей между массивами данных (таймсериями). В ней предложен готовый класс элемента управления, который включает набор графиков для анализа попарных зависимостей между несколькими символами. Таким образом, достаточно модифицировать его для работы с единственным графиком в "контроле" и тем самым будет получен требуемый результат.

Исходные коды статьи устанавливаются в каталог Include\PairPlot\. Файл, содержащий интересующий нас класс, называется PairPlot.mqh. На его основе мы создадим свой вариант, под именем Plot.mqh. Основные отличия будут в следующем.

Нам не понадобится класс CTimeserie — удалим его. Класс CPairPlot, являющийся "контролом", производным от CWndClient, преобразуем в CPlot, заменив в нем всю работу с массивом кросс-символьных графиков на один единственный график. Непосредственно графики в том проекте отрисовываются с помощью специальных классов гистограммы (CHistogram) и диаграммы рассеивания (CScatter), которые являются производными от общего базового класса CPlotBase, в свою очередь являющегося наследником CGraphic. Мы преобразуем CPlotBase в свой собственный класс CGraphicInPlot, также унаследованный от CGraphic, а особые гистограммы и диаграммы рассеивания нам не пригодятся. Вместо них мы будем пользоваться стандартными стилями отрисовки (CURVE_POINTS, CURVE_LINES, CURVE_POINTS_AND_LINES, CURVE_STEPS, CURVE_HISTOGRAM), предоставляемыми классом CGraphic, а точнее, его подопечным — CCurve. Упрощенная диаграмма связей классов приведена ниже.

Диаграмма связей "графических" классов

Диаграмма связей "графических" классов

Серым цветом выделены добавляемые классы, остальные — стандартные.

Для проверки работы нового элемента управления создадим тестовый эксперт PlotDemo. Как обычно, инициализация, привязка к событиям и запуск производятся в файле PlotDemo.mq5, а описание диалога вынесено в PlotDemo.mqh (оба файла прилагаются).

Эксперт принимает единственный входной параметр — стиль отрисовки.

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

В заголовочном файле диалога создаем объект нашего "контрола" и добавляем две тестовых кривых.

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

Как работает этот эксперт, можно увидеть в следующей анимации:

Демонстрация элемента управления с деловой графикой

Демонстрация элемента управления с деловой графикой

Итак, мы выполнили большой объем работы, и теперь возможности построения адаптивного интерфейса с поддержкой графики отвечают нашим требованиям в рамках проекта OLAP. В целях обобщения приведем диаграмму основных классов, связанных с графическим интерфейсом пользователя.

Диаграмма классов элементов управления

Диаграмма классов элементов управления

Белым цветом обозначены стандартные классы, желтым — классы контейнеров, розовым — классы диалогов и кастомизированных элементов, поддерживающих изменение размера, зеленым – "контрол" со встроенной деловой графикой.

Графический интерфейс для OLAP

Интерактивной обработкой данных торговой истории и их визуализацией будет заниматься новый эксперт — OLAPGUI. Все операции по созданию окна, "контролов", реакции на действия пользователя и вызову функций OLAP сведены в заголовочном файле OLAPGUI.mqh.

Во входных переменных эксперта оставим только то, что относится к импорту данных из HTML или CSV. В первую очередь это касается переменных ReportFile, Prefix, Suffix, уже знакомым нам по первому проекту OLAPDEMO. Если ReportFile оставить пустым, будет анализироваться торговая история текущего счета.

Непосредственно выбор селекторов, агрегаторов и стиля графика будем осуществлять с помощью элементов управления. По-прежнему оставим возможность задать 3 размерности гиперкуба, то есть 3 селектора по условным осям X, Y, Z. Для этого потребуется 3 выпадающих списка. Разместим их в первом, верхнем ряду "контролов". В этом же ряду у правого края сделаем кнопку Process, по нажатию на которую будет запускаться анализ.

Выбор функции агрегатора и поля, по которому производится агрегирование, реализуем с помощью двух других выпадающих списков — во втором ряду "контролов". Там же добавим выпадающий список для порядка сортировки и стиля графика. Фильтрацию исключим из проекта в целях упрощения интерфейса.

Всё оставшееся пространство окна будет занимать график.

Выпадающие списки с селекторами будут содержать одинаковый набор опций. В нем мы объединим типы селекторов и выводимых напрямую полей записей. В следующей таблице приведены названия элементов и соответствующие им поля и/или типы селекторов.

  • (selector/field), FIELD_NONE
  • ordinal [SerialNumberSelector], FIELD_NUMBER
  • symbol [SymbolSelector], FIELD_SYMBOL
  • type [TypeSelector], FIELD_TYPE
  • magic number [MagicSelector], FIELD_MAGIC
  • day of week open [WeekDaySelector], FIELD_DATETIME1
  • day of week close [WeekDaySelector], FIELD_DATETIME2
  • hour of day open [DayHourSelector], FIELD_DATETIME1
  • hour of day close [DayHourSelector], FIELD_DATETIME2
  • duration [DaysRangeSelector], FIELD_DATETIME1 и FIELD_DATETIME2
  • lot [TradeSelector/QuantizationSelector*], FIELD_LOT
  • profit [TradeSelector/QuantizationSelector*], FIELD_PROFIT_AMOUNT
  • profit percent [TradeSelector/QuantizationSelector*], FIELD_PROFIT_PERCENT
  • profit points [TradeSelector/QuantizationSelector*], FIELD_PROFIT_POINT
  • commission [TradeSelector/QuantizationSelector*], FIELD_COMMISSION
  • swap [TradeSelector/QuantizationSelector*], FIELD_SWAP
  • custom 1 [TradeSelector/QuantizationSelector*], FIELD_CUSTOM1
  • custom 2 [TradeSelector/QuantizationSelector*], FIELD_CUSTOM2

Выбор селекторов, помеченных *, определяется типом агрегатора: в случае IdentityAggregator используется TradeSelector, иначе — QuantizationSelector.

Названия селекторов (пункты от 1 до 9) в выпадающем списке заключены в кавычки.

Селекторы должны выбираться последовательно слева направо, от X к Z. Пока не выбран селектор предыдущего измерения, "комбобоксы" для последующих осей скрыты.

Поддерживаемые агрегатные функции:

  • sum
  • average
  • max
  • min
  • count
  • profit factor
  • progressive total
  • identity

Все, кроме последнего, требуют указания агрегируемого поля записи с помощью выпадающего списка справа от агрегатора.

Функция "progressive total" подразумевает, что селектором по оси X выбран "ordinal" (последовательный обход записей).

"Комбобокс" с сортировкой доступен, только если выбран единственный селектор (X).

Оси X и Y расположены на графике по горизонтали и вертикали, соответственно. Для трехмерных гиперкубов, имеющих разные координаты по оси Z, был применен самый примитивный из возможных подходов: множественные сечения в плоскости Z можно пролистывать с помощью кнопки Process. При наличии Z-координат, кнопка меняет название на "i / n title >>", где i — номер текущей Z-координаты, n — общее количество отсчетов по оси Z, title — название отсчета (например, день недели или тип сделок, в зависимости от выбранного по оси Z селектора). Если поменять условия построения гиперкуба, кнопка снова получает заголовок "Process" и начинает работать в штатном режиме. Обратите внимание, что при работе агрегатора "identity" обработка отличается: в этом случае куб всегда имеет 2 размерности, и все три кривые (для полей X, Y, Z) выводятся на графике вместе, без пролистывания.

Каждый куб не только отображается графически, но и выводится в текстовом виде в лог. Это особенно важно для тех случаев, когда агрегирование производится по простым полям, а не селекторам. Селекторы обеспечивают вывод меток по осям, а при квантизации простого поля система может лишь вывести индекс ячейки. Например, если мы хотим проанализировать прибыль в разбивке по размерам лотов, то можем выбрать поле "lot" в селекторе X, и агрегатор "sum" по полю "profit amount". При этом на оси X могут появиться отсчеты для 0, 0.5, 1, 1.0, 1.5 и т.д. вплоть до количества разных объемов, которыми производилась торговля. Однако это будут именно номера ячеек, а не значения лотов — последние можно увидеть в логе. Там будет написано примерно следующее:

	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"

Здесь value — это суммарная прибыль, title — реальный размер лота, соответствующий этой прибыли, а номера слева — координаты на оси X. Обратите внимание, что на графике вдоль оси могут выводиться и дробные значения, несмотря на то, что смысл имеют только целочисленные индексы. Этот и другие аспекты вывода меток, несомненно, могут быть улучшены.

Для того чтобы связать элементы управления графического интерфейса с ядром OLAP, которое мы оставим неизменным с первой статьи в заголовочном файле OLAPcube.mqh, нам потребуется реализовать класс-прослойку OLAPWrapper. В нем фактически будет организована та же подготовительная работа с данными, которую выполняла функция process в первом демонстрационном проекте OLAPDEMO, но теперь это будет метод класса.

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

С полным исходным кодом можно ознакомиться в приложении. Обратите внимание, что все настройки, которые в проекте OLAPDEMO мы получали из входных переменных, теперь передаются в виде параметров метода process, и они, очевидно, должны заполняться на основе состояния элементов управления.

Особый интерес представляет параметр display. Ядро OLAP декларирует этот специальный интерфейс Display для визуализации данных, и нам нужно его реализовать в графической части программы. Создав объект с этим интерфейсом, мы "внедрим зависимость" (dependency injection), о которой говорили в первой статье. Это позволит без изменения ядра OLAP подключить к нему новый способ отображения результатов.

В файле OLAPGUI.mq5 создадим диалог, передав в него экземпляр 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;
  }
  ...

Класс диалога OLAPDialog определен в 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);
      ...

В ответ на нажатие кнопки "Process", диалог на основе положения "контролов" заполняет необходимые параметры для метода OLAPWrapper::process и вызывает его, передав в качестве дисплея объект olapdisplay:

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

Полный код настройки всех параметров можно посмотреть в приложении.

Вспомогательный класс OLAPDisplay нам требуется потому, что MQL не поддерживает множественное наследование. Класс OLAPDialog является производным от MaximizableAppDialog и потому не может напрямую реализовывать интерфейс Dialog. Вместо этого мы делегируем эту задачу классу OLAPDisplay, а его объект создаем внутри окна и снабжаем ссылкой на создателя через параметр конструктора.

После построения куба, ядро OLAP вызывает метод 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;
    }
    ...

Суть происходящего в том, чтобы получить из объекта metaData требующие отображения данные (getDimension(), getDimensionTitle(), getVector()) и передать их окну. В приведенном фрагменте виден вариант обработки случая с единственным селектором. В классе нашего диалога зарезервированы специальные методы для приема данных:

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

Вот примеры некоторых аналитических разрезов, которые можно получить в графическом представлении с помощью OLAPGUI.

Прибыли по символам в порядке убывания величины

Прибыли по символам в порядке убывания величины

Прибыли по символам в алфавитном порядке

Прибыли по символам в алфавитном порядке

Прибыли по символу, дню недели закрытия, тип "покупка"

Прибыли по символу, дню недели закрытия, тип "покупка"

Прибыли по символу, дню недели закрытия, тип "продажа"

Прибыли по символу, дню недели закрытия, тип "продажа"

Прибыли по размеру лота (лоты указаны как индексы ячеек, значения выведены в лог)

Прибыли по размеру лота (лоты указаны как индексы ячеек, значения выведены в лог)

Общая кривая баланса

Общая кривая баланса

Баланс в разрезе покупок и продаж

Баланс в разрезе покупок и продаж

Кривые баланса для каждого символа отдельно

Кривые баланса для каждого символа отдельно

Кривые свопов для каждого символа отдельно

Кривые свопов для каждого символа отдельно

Зависимость прибылей от длительности трейда для каждого символа отдельно

Зависимость прибылей от длительности трейда для каждого символа отдельно

Количество сделок в разбивке по символам и типам

Количество сделок в разбивке по символам и типам

Зависимость полей прибыль и длительность для каждой сделки (длительность указана в секундах)

Зависимость полей прибыль и длительность для каждой сделки (длительность указана в секундах)

Зависимость MFE (%) и MAE (%) по всем сделкам

Зависимость MFE (%) и MAE (%) по всем сделкам

К сожалению, стандартный стиль рисования гистограммы не предусматривает отображения нескольких массивов со смещением столбиков одного и того же индекса из разных массивов. Иными словами, значения с одной и той же координатой могут полностью перекрывать друг друга. Для решения этой проблемы следует реализовать собственный способ визуализации гистограммы (что позволяет делать класс CGraphic), но это оставлено для самостоятельной работы.

Заключение

В данной статье мы рассмотрели общие принципы построения графического интерфейса пользователя для MQL-программ, поддерживающих изменение размера и универсальную раскладку элементов управления. На базе этой технологии было создано интерактивное приложение для анализа торговых отчетов, использующее наработки первой статьи из серии про OLAP. Визуализация различных показателей в произвольных сочетаниях и разрезах позволяет выявлять скрытые закономерности и упрощает многокритериальный анализ в целях оптимизации торговых систем.

Далее приведены перечни прилагаемых файлов по проектам.

Проект OLAPGUI

  • Experts/OLAP/OLAPGUI.mq5 — демонстрационный эксперт;
  • Experts/OLAP/OLAPGUI.mqh — описание графического интерфейса;
  • Include/OLAP/OLAPcore.mqh — связка графического интерфейса с ядром OLAP;
  • Include/OLAP/OLAPcube.mqh — основной заголовочный файл с классами OLAP;
  • Include/OLAP/PairArray.mqh — класс массива пар [значение;название] с поддержкой всех вариантов сортировки;
  • Include/OLAP/HTMLcube.mqh — сопряжение OLAP с загрузкой данных из HTML-отчетов;
  • Include/OLAP/CSVcube.mqh — сопряжение OLAP с загрузкой данных из CSV-файлов;
  • Include/MT4orders.mqh — библиотека MT4orders для работы с ордерами в едином стиле в МТ4 и в МТ5;
  • Include/Layouts/Box.mqh — контейнер элементов управления;
  • Include/Layouts/ComboBoxResizable.mqh — элемент управления выпадающий список с возможностью динамического изменения размера;
  • Include/Layouts/MaximizableAppDialog.mqh — окно диалога с возможностью динамического изменения размера;
  • Include/PairPlot/Plot.mqh — элемент управления с деловой графикой, поддерживающий динамическое изменение размера;
  • Include/Marketeer/WebDataExtractor.mqh — парсер HTML;
  • Include/Marketeer/empty_strings.h — список пустых тегов HTML;
  • Include/Marketeer/HTMLcolumns.mqh — определения индексов колонок в HTML-отчетах;
  • Include/Marketeer/CSVReader.mqh — парсер CSV;
  • Include/Marketeer/CSVcolumns.mqh — определения индексов колонок в CSV-отчетах;
  • Include/Marketeer/IndexMap.mqh — вспомогательный заголовочный файл с реализацией массива с комбинированным доступом по ключу и индексу;
  • Include/Marketeer/RubbArray.mqh — вспомогательный заголовочный файл с "резиновым" массивом;
  • Include/Marketeer/TimeMT4.mqh — вспомогательный заголовочный файл с реализацией функций работы с датами в стиле MT4;
  • Include/Marketeer/Converter.mqh — вспомогательный заголовочный файл с объединением для конвертации типов данных;
  • Include/Marketeer/GroupSettings.mqh — вспомогательный заголовочный файл групповых настроек входных параметров;

Проект SlidingPuzzle3

  • Experts/Examples/Layouts/SlidingPuzzle3.mq5
  • Experts/Examples/Layouts/SlidingPuzzle3.mqh
  • Include/Layouts/GridTk.mqh
  • Include/Layouts/Grid.mqh
  • Include/Layouts/Box.mqh

Проект Controls3

  • Experts/Examples/Layouts/Controls3.mq5
  • Experts/Examples/Layouts/ControlsDialog3.mqh
  • Include/Layouts/Box.mqh
  • Include/Layouts/SpinEditResizable.mqh
  • Include/Layouts/ComboBoxResizable.mqh
  • Include/Layouts/MaximizableAppDialog.mqh

Проект PlotDemo

  • Experts/Examples/Layouts/PlotDemo.mq5
  • Experts/Examples/Layouts/PlotDemo.mqh
  • Include/OLAP/PairArray.mqh
  • Include/Layouts/MaximizableAppDialog.mqh
Прикрепленные файлы |
MQLOLAP2.zip (73.86 KB)
Stanislav Korotky
Stanislav Korotky | 14 май 2019 в 16:25
fxsaber:


Скомпилировал через

#define private public

Лучше на protected.

fxsaber
fxsaber | 14 май 2019 в 16:28
Stanislav Korotky:

В статье указано: делаем патч библиотеки - самый простой способ меняем private на protected в соответствующих файлах. На всякий случай предварительно делаем бэкап.

Ну кто же читает инструкциистатьи перед применением...

Реter Konow
Реter Konow | 20 май 2019 в 13:33
Stanislav Korotky:
На вопрос в данной формулировке мне трудно ответить. Темой был OLAP. При нем GUI - утилитарная необходимость, поэтому он выполнен в минимальном объеме в виде надстройки стандартной библиотеки. Нечто тяжелое и меняющееся (из-за чего документация - не единая, а в виде кучи исправлений) было решено не использовать. Поскольку интерфейс вывода данных простой, желающие могут взять свой любимый GUI.
Хорошая надстройка.
TheXpert
TheXpert | 22 май 2019 в 10:32
Alexander Fedosov:
В чем отличие от EasyAndFast? 
вы в статью заглядывали или просто картинки посмотрели? )
Alexander Fedosov
Alexander Fedosov | 22 май 2019 в 11:21
Библиотека для простого и быстрого создания программ для MetaTrader (Часть VIII): События модификации ордеров и позиций Библиотека для простого и быстрого создания программ для MetaTrader (Часть VIII): События модификации ордеров и позиций

В предыдущих статьях мы начали создавать большую кроссплатформенную библиотеку, целью которой является упростить написания программ для платформы MetaTrader 5 и MetaTrader 4. В седьмой части мы добавили отслеживание событий срабатывания StopLimit-ордеров и подготовили функционал для отслеживания остальных событий, происходящих с ордерами и позициями. В данной статье сделаем класс для отслеживания событий модификации рыночных ордеров и позиций.

Исследование методов свечного анализа (Часть IV): Обновление и дополнение приложения Исследование методов свечного анализа (Часть IV): Обновление и дополнение приложения

В этой статье представлена следующая версия приложения Pattern Analyzer. В нем были исправлены некоторые недоработки, добавлены новые возможности, пересмотрено удобство и актуальность текущего интерфейса. При этом были рассмотрены пожелания и идеи из комментариев предыдущих статей. Что в итоге получилось — читайте далее в этой статье.

Библиотека для простого и быстрого создания программ для MetaTrader (Часть IX): Совместимость с MQL4 - Подготовка данных Библиотека для простого и быстрого создания программ для MetaTrader (Часть IX): Совместимость с MQL4 - Подготовка данных

В предыдущих статьях мы начали создавать большую кроссплатформенную библиотеку, целью которой является упростить написания программ для платформы MetaTrader 5 и MetaTrader 4. В восьмой части сделали класс для отслеживания событий модификации рыночных ордеров и позиций. В данной статье начнём доработку библиотеки с целью полной её совместимости с MQL4.

Создание графических интерфейсов на базе .Net Framework и C# (Часть 2): Дополнительные графические элементы Создание графических интерфейсов на базе .Net Framework и C# (Часть 2): Дополнительные графические элементы

Статья является логическим продолжением предыдущей публикации "Создание графических интерфейсов для экспертов и индикаторов на базе .Net Framework и C#" и знакомит читателей с новыми графическими элементами для создания графических интерфейсов.