Применение OLAP в трейдинге (Часть 4): Количественный и визуальный анализ отчетов тестера

28 февраля 2020, 16:55
Stanislav Korotky
8
2 635

В данной статье мы продолжим знакомство с технологией OLAP (On-Line Analytical Processing, интерактивной аналитической обработки) в приложении к трейдингу.

В предыдущих статьях были описаны общие приемы построения классов для аккумулирования и анализа многомерных данных, а также визуализация результатов анализа в графическом интерфейсе. С прикладной точки зрения в первых двух статьях речь шла о торговых отчетах, полученных различными способами: из тестера стратегий, из истории онлайн-торговли, из файлов в форматах HTML и CSV (включая торговые сигналы MQL5). В третьей статье, после небольшого рефакторинга кода, OLAP был использован для анализа котировок и выработки торговых стратегий. Для хорошего понимания нового материала необходимо ознакомиться с этими статьями (в скобках указано, на что в первую очередь обратить внимание):

Сегодня мы расширим область применения OLAP за счет анализа результатов оптимизации MetaTrader 5.

Для выполнения этого проекта нам предварительно потребуется слегка усовершенствовать графический пользовательский интерфейс, рассмотренный в статье 2. Дело в том, что улучшения в коде, произведенные в статье 3, ограничились непосредственно движком OLAP и визуализация не подвергалась "апгрейду". Поправим это упущение, используя в качестве тестовой задачи знакомую из второй статьи программу OLAPGUI для анализа торговых отчетов. Попутно унифицируем эту же графическую часть таким образом, чтобы её легко было состыковать с любым новым прикладным направлением, в частности с планируемым анализатором результатов оптимизации.

Деловая графика под прицелом

Напомним, что центром GUI для OLAP является специально разработанный визуальный компонент CGraphicInPlot. Его первая реализация, представленная в статье 2, страдала от некоторых недочетов. Прежде всего это касалось вывода меток на осях. Мы научились при необходимости отображать названия ячеек селекторов (такие как названия дней недели или валют) на горизонтальной оси X, но во всех остальных случаях график подразумевает вывод числа "как есть", что не всегда удобочитаемо (или "дружественно пользователю"). Кроме того, для оси Y, где обычно выводятся агрегированные значения, но в зависимости от настроек могут выводиться и ячейки селекторов, также требуется кастомизация. В качестве примера неудобного отображения меток хорошо подходит запрос на среднее удержание позиции по символу.

Средняя длительность позиции по символам (секунды)

Средняя длительность позиции по символам (секунды)

Поскольку по оси Y здесь выводится не селектор (округляющий значения до размера ячеек куба), а агрегированное значение длительности в секундах, большие числа плохо воспринимаются. Чтобы решить данную проблему попробуем делить секунды на длительность бара текущего таймфрейма. Тогда отображаемые значения покажут количество баров. Для этого требуется передать в класс CGraphicInPlot и далее во встроенный класс обработки осей CAxis некий флаг. Таких флагов, меняющих режим работы, может быть много, поэтому зарезервируем для них особый новый класс AxisCustomizer в файле Plot.mqh.

  class AxisCustomizer
  {
    public:
      const CGraphicInPlot *parent;
      const bool y; // true for Y, false for X
      const bool periodDivider;
      const bool hide;
      AxisCustomizer(const CGraphicInPlot *p, const bool axisY,
        const bool pd = false, const bool h = false):
        parent(p), y(axisY), periodDivider(pd), hide(h) {}
  };

Потенциально в него можно будет добавлять многие нюансы отображения меток, но в данный момент он хранит лишь признак типа оси (X или Y) и несколько логических опций, таких как periodDivider и hide. Первая из них означает, что требуется разделить величины на PeriodSeconds(), вторая будет рассмотрена позднее.

Объекты данного класса попадают в CGraphicInPlot с помощью специальных методов:

  class CGraphicInPlot: public CGraphic
  {
    ...
      void InitAxes(CAxis &axe, const AxisCustomizer *custom = NULL);
      void InitXAxis(const AxisCustomizer *custom = NULL);
      void InitYAxis(const AxisCustomizer *custom = NULL);
  };
  
  void CGraphicInPlot::InitAxes(CAxis &axe, const AxisCustomizer *custom = NULL)
  {
    if(custom)
    {
      axe.Type(AXIS_TYPE_CUSTOM);
      axe.ValuesFunctionFormat(CustomDoubleToStringFunction);
      axe.ValuesFunctionFormatCBData((AxisCustomizer *)custom);
    }
    else
    {
      axe.Type(AXIS_TYPE_DOUBLE);
    }
  }
  
  void CGraphicInPlot::InitXAxis(const AxisCustomizer *custom = NULL)
  {
    InitAxes(m_x, custom);
  }
  
  void CGraphicInPlot::InitYAxis(const AxisCustomizer *custom = NULL)
  {
    InitAxes(m_y, custom);
  }

Когда подобный объект не создается и не передается в графические классы, стандартная библиотека выводит значения обычным образом — как число (AXIS_TYPE_DOUBLE).

Здесь мы используем подход стандартной библиотеки для кастомизации надписей на осях: тип оси устанавливается равным AXIS_TYPE_CUSTOM и с помощью ValuesFunctionFormatCBData передается указатель на объект AxisCustomizer. Он впоследствии передается базовым классом CGraphic в нашу функцию отрисовки метки CustomDoubleToStringFunction (она назначается вызовом ValuesFunctionFormat во фрагменте кода выше). Разумеется, нам потребуется сама функция CustomDoubleToStringFunction — она уже была реализована ранее, но в упрощенном виде, без использования объектов выделенного класса AxisCustomizer (таким настроечным объектом выступал сам график CGraphicInPlot).

  string CustomDoubleToStringFunction(double value, void *ptr)
  {
    AxisCustomizer *custom = dynamic_cast<AxisCustomizer *>(ptr);
    if(custom == NULL) return NULL;
    
    // check options
    if(!custom.y && custom.hide) return NULL; // case of X axis and "no marks" mode
    
    // in simple cases return a string
    if(custom.y) return (string)(float)value;  
    
    const CGraphicInPlot *self = custom.parent; // obtain actual object with cache 
    if(self != NULL)
    {
      ... // retrieve selector mark for value
    }
  }

Непосредственно объекты кастомизации AxisCustomizer хранятся в классе CPlot, являющемся интерфейсным элементом управления (наследником CWndClient) и контейнером для CGraphicInPlot:

  class CPlot: public CWndClient
  {
    private:
      CGraphicInPlot *m_graphic;
      ENUM_CURVE_TYPE type;
      
      AxisCustomizer *m_customX;
      AxisCustomizer *m_customY;
      ...
    
    public:
      void InitXAxis(const AxisCustomizer *custom = NULL)
      {
        if(CheckPointer(m_graphic) != POINTER_INVALID)
        {
          if(CheckPointer(m_customX) != POINTER_INVALID) delete m_customX;
          m_customX = (AxisCustomizer *)custom;
          m_graphic.InitXAxis(custom);
        }
      }
      ...
  };

Таким образом, настройки осей в объектах m_customX и m_customY могут использоваться не только на поздней стадии форматирования величин в функции CustomDoubleToStringFunction, но и гораздо раньше — когда массивы с данными еще только передаются в CPlot с помощью одного из методов CurveAdd, например, так:

  CCurve *CPlot::CurveAdd(const PairArray *data, const string name = NULL)
  {
    if(CheckPointer(m_customY) != POINTER_INVALID) && m_customY.periodDivider)
    {
      for(int i = 0; i < ArraySize(data.array); i++)
      {
        data.array[i].value /= PeriodSeconds();
      }
    }
    
    return m_graphic.CurveAdd(data, type, name);
  }

Здесь мы как раз видим применение опции periodDivider для деления всех значений на PeriodSeconds(). Эта операция выполняется до того, как стандартная библиотека получит данные и рассчитает для них размер шага сетки. Это важно, потому что после того, как сетка уже посчитана, в функции CustomDoubleToStringFunction поздно производить кастомизацию, связанную с масштабированием.

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

  AGGREGATORS at = ...  // get aggregator type from GUI
  ENUM_FIELDS af = ...  // get aggregator field from GUI
  SORT_BY sb = ...      // get sorting mode from GUI
  
  int dimension = 0;    // calculate cube dimensions from GUI
  for(int i = 0; i < AXES_NUMBER; i++)
  {
    if(Selectors[i] != SELECTOR_NONE) dimension++;
  }
  
  bool hideMarksOnX = (dimension > 1 && SORT_VALUE(sb));
  
  AxisCustomizer *customX = NULL;
  AxisCustomizer *customY = NULL;
  
  customX = new AxisCustomizer(m_plot.getGraphic(), false, Selectors[0] == SELECTOR_DURATION, hideMarksOnX);
  if(af == FIELD_DURATION)
  {
    customY = new AxisCustomizer(m_plot.getGraphic(), true, true);
  }
  
  m_plot.InitXAxis(customX);
  m_plot.InitYAxis(customY);

Здесь m_plot — переменная диалога, в которой хранится элемент управления CPlot. Далее будет приведен полный код метода OLAPDialog::process, где будет видно, как это делается на самом деле. А вот как выглядит вышеприведенный пример графика с автоматически включенным режимом periodDivider:

Средняя длительность позиции по символам (бары текущего таймфрейма, D1)

Средняя длительность позиции по символам (бары текущего таймфрейма, D1)

Другая переменная в AxisCustomizer — hide — предусматривает возможность полностью скрывать метки по оси X. Этот режим нужен для случая, когда пользователь выбирает сортировку по значению в многомерном кубе. Тогда метки в каждом ряду чисел будут иметь свой собственный порядок и выводить вдоль оси X нечего. Сортировка в многомерном кубе доступна, потому что имеет смысл в других режимах, в частности по меткам.

Работа опции hide происходит внутри CustomDoubleToStringFunction. Стандартное поведение этой функции подразумевает наличие селекторов: их метки кэшируются для оси X в специальном классе CurveSubtitles и возвращаются в график по индексу деления сетки. Однако взведенный флаг hide обрывает этот процесс в самом начале для любой абсциссы, и функция возвращает NULL (неотображаемое значение).

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

В базовом классе CGraphic имеется виртуальный метод HistogramPlot. Его необходимо переопределить и каким-то образом визуально "развести" столбики одного отсчета. Для этой цели желательно было бы иметь в объекте кривой CCurve пользовательское поле, хранящее произвольные данные (интерпретируемые клиентским кодом как ему требуется). К сожалению, такого поля нет и потому придется использовать одно из стандартных свойств, не задействованное в текущем проекте. Выбор пал на LinesSmoothStep. С помощью метода-"сеттера" CCurve::LinesSmoothStep наш вызывающий код будет записывать туда порядковый номер ряда, а с помощью метода-"геттера" CCurve::LinesSmoothStep его легко получить в новой реализации HistogramPlot. Вот пример записи номера ряда в LinesSmoothStep:

  CCurve *CGraphicInPlot::CurveAdd(const double &x[], const double &y[], ENUM_CURVE_TYPE type, const string name = NULL)
  {
    CCurve *c = CGraphic::CurveAdd(x, y, type, name);
    c.LinesSmoothStep((int)CGraphic::CurvesTotal());    // +
    ...
    return CacheIt(c);
  }

Зная общее количество рядов и номер текущего, можно сместить каждую его точку немного влево или вправо от центра отсчета при отрисовке. Вот адаптированный вариант HistogramPlot. Измененные строки помечены комментариями с символом "*", добавленные — с символом "+".

  void CGraphicInPlot::HistogramPlot(CCurve *curve) override
  {
      const int size = curve.Size();
      const double offset = curve.LinesSmoothStep() - 1;                   // +
      double x[], y[];
  
      int histogram_width = curve.HistogramWidth();
      if(histogram_width <= 0) return;
      
      curve.GetX(x);
      curve.GetY(y);
  
      if(ArraySize(x) == 0 || ArraySize(y) == 0) return;
      
      const int w = m_width / size / 2 / CGraphic::CurvesTotal();          // +
      const int t = CGraphic::CurvesTotal() / 2;                           // +
      const int half = ((CGraphic::CurvesTotal() + 1) % 2) * (w / 2);      // +
  
      int originalY = m_height - m_down;
      int yc0 = ScaleY(0.0);
  
      uint clr = curve.Color();
  
      for(int i = 0; i < size; i++)
      {
        if(!MathIsValidNumber(x[i]) || !MathIsValidNumber(y[i])) continue;
        int xc = ScaleX(x[i]);
        int yc = ScaleY(y[i]);
        int xc1 = xc - histogram_width / 2 + (int)(offset - t) * w + half; // *
        int xc2 = xc + histogram_width / 2 + (int)(offset - t) * w + half; // *
        int yc1 = yc;
        int yc2 = (originalY > yc0 && yc0 > 0) ? yc0 : originalY;
  
        if(yc1 > yc2) yc2++;
        else yc2--;
  
        m_canvas.FillRectangle(xc1,yc1,xc2,yc2,clr);
      }
  }

Скоро мы проверим, как это выглядит.

Еще один неприятный момент связан со стандартной реализацией вывода линий. Если в данных встречается не число, CGraphic прерывает линию. Для нашей задачи это плохо, т.к. в некоторых ячейках куба действительно может не быть данных, и агрегаторы в таком случае записывают туда NaN. Некоторые кубы, например, такие как баланс нарастающим итогом по нескольким сечениям, по своей сути являются разряженными, потому что в каждой сделке меняется сумма лишь в одном сечении. Чтобы понять, насколько негативно разрыв линий сказывается на восприятии достаточно взглянуть на рисунок "Кривые баланса для каждого символа отдельно" в статье 2.

Для решения этой проблемы был дополнительно переопределен метод LinesPlot (см. исходные коды, файл Plot.mqh). Результат работы продемонстрирован чуть ниже, в разделе обработки штатных файлов тестера.

Наконец, последняя проблема с графикой относится к определению нулевых осей в Стандартной библиотеке. В методе CGraphic::CreateGrid поиск нулей выполняется следующим тривиальным образом (приведен случай оси Y, ось X обрабатывается аналогично):

  if(StringToDouble(m_yvalues[i]) == 0.0)
  ...

Обратите внимание, что m_yvalues здесь — строковые метки. Очевидно, что любая метка, не содержащая число, будет давать 0. Это происходит даже в том случае, если для графика задан тип отображения AXIS_TYPE_CUSTOM, как в нашем случае. В результате, при отображении графика по валютам, дням недели, типам сделок и прочим селекторам, все отсчеты последовательно трактуются нулевыми по мере того как они проверяются в цикле по всей сетке, но "слово остается" за последним отсчетом, и он выделяется на графике более жирной прямой (хотя не является нулем). Более того, когда каждый отсчет становится, пусть и временно, кандидатом на 0, он пропускает отрисовку обычной линии сетки, из-за чего вся сетка просто исчезает.

Поскольку метод CreateGrid также является виртуальным, мы переопределим его с более интеллектуальной проверкой на 0, которую вынесем во вспомогательную функцию isZero.

  bool CGraphicInPlot::isZero(const string &value)
  {
    if(value == NULL) return false;
    double y = StringToDouble(value);
    if(y != 0.0) return false;
    string temp = value;
    StringReplace(temp, "0", "");
    ushort c = StringGetCharacter(temp, 0);
    return c == 0 || c == '.';
  }
  
  void CGraphicInPlot::CreateGrid(void) override
  {
    int xc0 = -1.0;
    int yc0 = -1.0;
    for(int i = 1; i < m_ysize - 1; i++)
    {
      m_canvas.LineHorizontal(m_left + 1, m_width - m_right, m_yc[i], m_grid.clr_line);     // *
      if(isZero(m_yvalues[i])) yc0 = m_yc[i];                                               // *
      
      for(int j = 1; j < m_xsize - 1; j++)
      {
        if(i == 1)
        {
          m_canvas.LineVertical(m_xc[j], m_height - m_down - 1, m_up + 1, m_grid.clr_line); // *
          if(isZero(m_xvalues[j])) xc0 = m_xc[j];                                           // *
        }
        
        if(m_grid.has_circle)
        {
          m_canvas.FillCircle(m_xc[j], m_yc[i], m_grid.r_circle, m_grid.clr_circle);
          m_canvas.CircleWu(m_xc[j], m_yc[i], m_grid.r_circle, m_grid.clr_circle);
        }
      }
    }
    
    if(yc0 > 0) m_canvas.LineHorizontal(m_left + 1, m_width - m_right, yc0, m_grid.clr_axis_line);
    if(xc0 > 0) m_canvas.LineVertical(xc0, m_height - m_down - 1, m_up + 1, m_grid.clr_axis_line);
  }

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

На этом мы завершили исправление недочетов в графике, но оконный интерфейс требует еще пересмотра в целях универсализации. В неторгующем эксперте OLAPGUI из второй статьи вся работа с диалогом была заключена в заголовочном файле OLAPGUI.mqh. В нем хранилось много прикладных особенностей прошлой задачи — анализа торговых отчетов. Поскольку мы собираемся использовать такой же диалог для произвольных данных, необходимо разбить данный файл на 2 части: в одну свести всё общее поведение интерфейса, а во вторую выделить настройки конкретного проекта.

Бывший класс OLAPDialog переименуем в OLAPDialogBase. Жестко "зашитые" в него статические массивы selectors, settings, defaults, которые по сути описывают элементы управления диалога, сделаем пустыми динамическими заготовками — их заполнением займутся производные классы. Переменные:

    OLAPWrapper *olapcore;    // <-- template <typename S,typename T> class OLAPEngine, since part 3
    OLAPDisplay *olapdisplay;

также "уйдут" в наследники, потому что их потребуется шаблонизировать типами селекторов и полей записей, которые определяются в прикладной части каждого OLAP движка. Напомним, что старый класс OLAPWrapper был преобразован в ходе рефакторинга в статье 3 в класс-шаблон OLAPEngine<S,T>.

Для выполнения основной работы зарезервированы 2 новых абстрактных метода:

  virtual void setup() = 0;
  virtual int process() = 0;

Первый (setup) производит настройку интерфейса, второй (process) запускает анализ. Вызов настройки производится из OLAPDialogBase::Create

  bool OLAPDialogBase::Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
  {
    setup(); // +
    ...
  }

Запуск анализа инициирует пользователь нажатием на кнопку, поэтому наибольшей переделке подвергся метод OLAPDialogBase::OnClickButton — из него удалена большая часть кода и соответствующий функционал (чтение свойств "контролов" и запуск на их основе OLAP-движка) делегирован тому самому методу process.

  void OLAPDialogBase::OnClickButton(void)
  {
    if(processing) return; // prevent re-entrancy
    
    if(browsing)           // 3D-cube browsing support
    {
      currentZ = (currentZ + 1) % maxZ;
      validateZ();
    }
  
    processing = true;
    const int n = process();
    if(n == 0 && processing)
    {
      finalize();
    }
  }

Обратите внимание, что класс OLAPDialogBase реализует всю логику работы интерфейса, начиная от создания элементов управления и заканчивая обработкой событий, влияющих на их состояние. Однако он ничего не знает о содержимом элементов управления.

Класс OLAPDisplay реализует виртуальный интерфейс Display из OLAPCommon.mqh (рассмотрен в 3-ей статье). Напомним, что интерфейс Display является точкой обратного вызова из ядра OLAP для предоставления результатов анализа (передаются в первом параметре, в объекте класса MetaCube). Благодаря указателю на родительское окно parent в классе OLAPDisplay организована цепочка дальнейшей передачи данных куба в диалог (такой "проброс" потребовался, потому что в MQL5 нет множественного наследования).

  class OLAPDisplay: public Display
  {
    private:
      OLAPDialogBase *parent;
  
    public:
      OLAPDisplay(OLAPDialogBase *ptr,): parent(ptr) {}
      virtual void display(MetaCube *metaData, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override;
  };

Здесь уместно затронуть нюанс, связанный с получением от производных классов адаптеров названий "настоящих" пользовательских полей. Дело в том, что до сих пор мы хоть и добавляли к стандартным полям предметной области свои "кастом" поля, такие, например, как MFE и MAE во 2-й статье, но по сути они были известны заранее и встраивались в код. Однако в случае, когда мы будем работать с отчетами оптимизации, потребуется анализировать их в разрезе входных параметров роботов, и эти параметры (их названия) можно получить только из анализируемых данных.

Адаптер передает названия "кастом"-полей в агрегатор (метакуб) с помощью нового метода assignCustomFields, причем происходит это "за кадром", то есть автоматически в методе Analyst::acquireData. Благодаря этому, когда внутри OLAPDisplay::display вызывается метод metaData.getDimensionTitle для получения обозначения сечений по осям и порядковый номер поля n превышает мощность встроенного перечисления полей, мы знаем, что имеем дело с расширенным полем и можем запросить описание у куба. Общая структура метода OLAPDisplay::display не поменялась — в этом легко убедиться, сравнив контекстно прилагаемые исходные коды с версией из статьи 2.

Кроме того, названия "кастом"-полей нужно знать заранее в диалоге, чтобы заполнить ими интерфейсные элементы. Для этого в класс OLAPDialogBase также добавлен новый метод для настройки "кастом" полей setCustomFields.

    int customFieldCount;
    string customFields[];
    
    virtual void setCustomFields(const DataAdapter &adapter)
    {
      string names[];
      if(adapter.getCustomFields(names) > 0)
      {
        customFieldCount = ArrayCopy(customFields, names);
      }
    }

Разумеется, нам потребуется "связать" вместе с помощью этого метода диалог и адаптер в тестовом эксперте (см. далее). После этого в элементах управления диалога станут видны осмысленные названия полей (а не номера custom 1 и т.д.). Это временное решение. Этот и некоторые другие аспекты требуют дальнейшей оптимизации кода, но в контексте данной статьи сочтены несущественными.

Прикладная часть настройки интерфейса для модифицируемой программы OLAPGUI "переехала" из OLAPGUI.mqh в заголовочный файл OLAPGUI_Trades.mqh. Название класса диалога осталось прежним — OLAPDialog, но он зависит от шаблонных параметров, которые затем используются при специализации объекта OLAPEngine:

  template<typename S, typename F>
  class OLAPDialog: public OLAPDialogBase
  {
    private:
      OLAPEngine<S,F> *olapcore;
      OLAPDisplay *olapdisplay;
  
    public:
      OLAPDialog(OLAPEngine<S,F> &olapimpl);
      ~OLAPDialog(void);
      virtual int process() override;
      virtual void setup() override;
  };
  
  template<typename S, typename F>
  OLAPDialog::OLAPDialog(OLAPEngine<S,F> &olapimpl)
  {
    curveType = CURVE_POINTS;
    olapcore = &olapimpl;
    olapdisplay = new OLAPDisplay(&this);
  }
  
  template<typename S, typename F>
  OLAPDialog::~OLAPDialog(void)
  {
    delete olapdisplay;
  }

Вся работа выполняется в методах setup и process. Метод setup заполняет массивы settings, selectors, defaults теми же значениями, что знакомы нам из 2-ой статьи (поскольку внешний вид интерфейса не меняется). Метод process запускает анализ в заданном разрезе и почти полностью повторяет бывший обработчик OnClickButton.

  template<typename S, typename F>
  int OLAPDialog::process() override
  {
    SELECTORS Selectors[4];
    ENUM_FIELDS Fields[4];
    AGGREGATORS at = (AGGREGATORS)m_algo[0].Value();
    ENUM_FIELDS af = (ENUM_FIELDS)(AGGREGATORS)m_algo[1].Value();
    SORT_BY sb = (SORT_BY)m_algo[2].Value();
  
    ArrayInitialize(Selectors, SELECTOR_NONE);
    ArrayInitialize(Fields, FIELD_NONE);
  
    int matches[10] = // selectors in combo-boxes (specific record fields are bound internally)
    {
      SELECTOR_NONE, SELECTOR_SERIAL, SELECTOR_SYMBOL, SELECTOR_TYPE, SELECTOR_MAGIC,
      SELECTOR_WEEKDAY, SELECTOR_WEEKDAY, SELECTOR_DAYHOUR, SELECTOR_DAYHOUR, SELECTOR_DURATION
    };
    
    int subfields[] = // record fields listed in combo-boxes after selectors and accessible directly  
    {
      FIELD_LOT, FIELD_PROFIT_AMOUNT, FIELD_PROFIT_PERCENT, FIELD_PROFIT_POINT,
      FIELD_COMMISSION, FIELD_SWAP, FIELD_CUSTOM_1, FIELD_CUSTOM_2
    };
    
    for(int i = 0; i < AXES_NUMBER; i++) // up to 3 orthogonal axes are supported
    {
      if(!m_axis[i].IsVisible()) continue;
      int v = (int)m_axis[i].Value();
      if(v < 10) // selectors (every one is specialized for a field already)
      {
        Selectors[i] = (SELECTORS)matches[v];
        if(v == 5 || v == 7) Fields[i] = FIELD_OPEN_DATETIME;
        else if(v == 6 || v == 8) Fields[i] = FIELD_CLOSE_DATETIME;
      }
      else // pure fields
      {
        Selectors[i] = at == AGGREGATOR_IDENTITY ? SELECTOR_SCALAR : SELECTOR_QUANTS;
        Fields[i] = (TRADE_RECORD_FIELDS)subfields[v - 10];
      }
    }
  
    m_plot.CurvesRemoveAll();
    AxisCustomizer *customX = NULL;
    AxisCustomizer *customY = NULL;
  
    if(at == AGGREGATOR_IDENTITY || at == AGGREGATOR_COUNT) af = FIELD_NONE;
    
    if(at != AGGREGATOR_PROGRESSIVE)
    {
      customX = new AxisCustomizer(m_plot.getGraphic(), false, Selectors[0] == SELECTOR_DURATION, (dimension > 1 && SORT_VALUE(sb)));
    }
    
    if((af == FIELD_DURATION)
    || (at == AGGREGATOR_IDENTITY && Selectors[1] == SELECTOR_DURATION))
    {
      customY = new AxisCustomizer(m_plot.getGraphic(), true, true);
    }
    
    m_plot.InitXAxis(customX);
    m_plot.InitYAxis(customY);
    m_button_ok.Text("Processing...");
    return olapcore.process(Selectors, Fields, at, af, olapdisplay, sb);
  }

Ближе к концу метода мы видим создание объектов настройки осей AxisCustomizer, которые были описаны ранее. Для обеих осей X и Y деление величин на PeriodSeconds() включается при работе с полем длительности (либо в агрегаторе, либо в селекторе, если тип агрегатора AGGREGATOR_IDENTITY — в этом случае содержимое полей не раскладывается селекторами по именованным ячейкам, а попадает в куб напрямую). Ось X отключается, когда размерность куба больше 1 и выбрана сортировка величин.

Осталось взглянуть на файл программы OLAPGUI.mq5. Среди отличий от старой версии — слегка изменившийся порядок подключения заголовочных файлов: если раньше адаптеры для отчетов были включены в ядро (потому что других источников данных еще не было), то теперь они должны явным образом прописываться как HTMLcube.mqh и CSVcube.mqh. Далее в коде OnInit выполняется подготовка соответствующего типа адаптера в зависимости от входных данных и передача его в движок с помощью вызова _defaultEngine.setAdapter. Этот фрагмент уже появлялся в программе OLAPRPRT.mq5 из статьи 3, где впервые тестировался правильный подход с декомпозицией на универсальную и прикладную части. Правда OLAPRPRT остался тогда без графического интерфейса, и мы сейчас постепенно приближаемся к ликвидации этой несправедливости.

В целях демонстрации строгого разделения стандартных и "кастом" полей записей, класс CustomTradeRecord с расчетом полей MFE и MAE был вынесен из OLAPTrades.mqh в OLAPTradesCustom.mqh (здесь не приводится, исходные коды прилагаются). Тем самым упрощается написание, при необходимости, других пользовательских полей на основе сделок — достаточно поменять алгоритм в OLAPTradesCustom.mqh, оставив ядро OLAP в неприкосновенности. Все стандартные "вещи": поля торговых записей, связанные с ними селекторы, базовый класс TradeRecord, движок OLAPEngineTrade, а также адаптер для истории счета остались в OLAPTrades.mqh. Разумеется, чтобы включить это всё в проект, в OLAPTradesCustom.mqh имеется ссылка на OLAPTrades.mqh.

  #include <OLAP/OLAPTradesCustom.mqh> // internally includes OLAPTrades.mqh 
  #include <OLAP/HTMLcube.mqh>
  #include <OLAP/CSVcube.mqh>
  #include <OLAP/GUI/OLAPGUI_trades.mqh>
  
  OLAPDialog<SELECTORS,ENUM_FIELDS> dialog(_defaultEngine);
  
  int OnInit()
  {
    if(ReportFile == "")
    {
      Print("Analyzing account history");
      _defaultEngine.setAdapter(&_defaultHistoryAdapter);
    }
    else
    {
      if(StringFind(ReportFile, ".htm") > 0 && _defaultHTMLReportAdapter.load(ReportFile))
      {
        _defaultEngine.setAdapter(&_defaultHTMLReportAdapter);
      }
      else
      if(StringFind(ReportFile, ".csv") > 0 && _defaultCSVReportAdapter.load(ReportFile))
      {
        _defaultEngine.setAdapter(&_defaultCSVReportAdapter);
      }
      else
      {
        Print("Unknown file format: ", ReportFile);
        return INIT_PARAMETERS_INCORRECT;
      }
    }
    
    ...
    
    if(!dialog.Create(0, "OLAPGUI" + (ReportFile != "" ? " : " + ReportFile : ""), 0,  0, 0, 750, 560)) return INIT_FAILED;
    
    if(!dialog.Run()) return INIT_FAILED;
    return INIT_SUCCEEDED;
  }

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

Увидеть изменения проще всего в сравнении с аналогичными скриншотами из 2-ой статьи. Вот как выглядит "Зависимость полей прибыль и длительность для каждой сделки" — теперь длительность по оси X выражена в барах текущего таймфрейма (в данном случае, это D1), а не секундах.

Зависимость прибыли сделок от длительности (в барах текущего таймфрейма, D1)

Зависимость прибыли сделок от длительности (в барах текущего таймфрейма, D1)

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

Прибыли в разрезе символов и дней недели

Прибыли в разрезе символов и дней недели

Анализ прибылей по размеру лота в сделках приведен на следующем скриншоте. В отличие от 2-ой статьи значения лотов отображены непосредственно на оси X, а не в логе.

Прибыли в разрезе размера лотов

Прибыли в разрезе размера лотов

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

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

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

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

Подключение стандартных файлов отчетов тестера (*.tst)

Разработчики MetaTrader 5 недавно открыли форматы файлов, сохраняемых тестером. В частности, данные об одиночном проходе, который мы до сих пор могли анализировать только после экспорта в HTML-отчет, теперь доступны для чтения непосредственно из tst-файла.

Мы не будем углубляться во внутреннее устройство файла. К счастью для нас уже существует библиотека для чтения tst-файлов SingleTesterCache (автор — fxsaber). Используя её по принципу "черного ящика", легко получить массив записей о торговых сделках. Сделка представлена в библиотеке классом TradeDeal. Чтобы получить их список достаточно подключить библиотеку, создать объект основного класса SINGLETESTERCACHE и загрузить требуемый файл методом load.

  #include <fxsaber/SingleTesterCache/SingleTesterCache.mqh>
  ...
  SINGLETESTERCACHE SingleTesterCache;
  if(SingleTesterCache.Load(file))
  {
    Print("Tester cache import: ", ArraySize(SingleTesterCache.Deals), " deals");
  }

Массив SingleTesterCache.Deals как раз и содержит все сделки, по каждой из которых в соответствующих полях доступна вся информация, имеющаяся в тестере.

Алгоритм генерации торговых позиций на основе сделок абсолютно такой же, как и при импорте HTML-отчета. Хороший стиль ООП предполагает, что надо вынести общие части кода в базовый класс, а затем унаследовать от него HTMLReportAdapter и новый TesterReportAdapter.

Сделаем общим предком отчетов класс BaseReportAdapter (файл ReportCubeBase.mqh). Вы можете сравнить этот файл контекстно с файлом HTMLcube.mqh старой инкарнации, чтобы убедиться, что в нем очень мало отличий (помимо переименования классов). Главное, что бросается в глаза — это минималистское наполнение метода load, который превратился в виртуальную заглушку:

    virtual bool load(const string file)
    {
      reset();
      TradeRecord::reset();
      return false;
    }

Наследники должны переопределить этот метод.

Также изменился код в методе generate, который собственно и преобразует сделки в позиции. Теперь в начале этого метода вызывается виртуальная "пустышка" fillDealsArray.

    virtual bool fillDealsArray() = 0;
    
    int generate()
    {
      ...
      if(!fillDealsArray()) return 0;
      ...
    }

Посмотрим, как часть существующего кода для работы с HTML-отчетами перенесена в новые виртуальные методы в классе HTMLReportAdapter. Обратите внимание: ниже представлен весь класс HTMLReportAdapter целиком — из-за того, что основная часть кода осталась в базовом классе, здесь нужно лишь определить 2 виртуальных метода.

  template<typename T>
  class HTMLReportAdapter: public BaseReportAdapter<T>
  {
    protected:
      IndexMap *data;
      
      virtual bool fillDealsArray() override
      {
        for(int i = 0; i < data.getSize(); ++i)
        {
          IndexMap *row = data[i];
          if(CheckPointer(row) == POINTER_INVALID || row.getSize() != COLUMNS_COUNT) return false; // something is broken
          string s = row[COLUMN_SYMBOL].get<string>();
          StringTrimLeft(s);
          if(StringLen(s) > 0) // there is a symbol -> this is a deal
          {
            array << new Deal(row);
          }
          else if(row[COLUMN_TYPE].get<string>() == "balance")
          {
            string t = row[COLUMN_PROFIT].get<string>();
            StringReplace(t, " ", "");
            balance += StringToDouble(t);
          }
        }
        return true;
      }
    
    public:
      ~HTMLReportAdapter()
      {
        if(CheckPointer(data) == POINTER_DYNAMIC) delete data;
      }
      
      virtual bool load(const string file) override
      {
        BaseReportAdapter<T>::load(file);
        if(CheckPointer(data) == POINTER_DYNAMIC) delete data;
        data = NULL;
        if(StringFind(file, ".htm") > 0)
        {
          data = HTMLConverter::convertReport2Map(file, true);
          if(data != NULL)
          {
            size = generate();
            Print(data.getSize(), " deals transferred to ", size, " trades");
          }
        }
        return data != NULL;
      }
  };

Код обоих методов нам знаком по прошлой версии, ничего менять не пришлось.

А теперь посмотрим на реализацию нового адаптера TesterReportAdapter. Прежде всего, здесь потребовалось добавить класс TesterDeal, производный от класса Deal, определенного в ReportCubeBase.mqh (Deal — это старый класс, он находился ранее в HTMLcube.mqh). TesterDeal имеет конструктор с параметром TradeDeal, что есть ни что иное, как сделка из библиотеки SingleTesterCache. Также в TesterDeal определена пара вспомогательных методов для конвертации перечислений типа и направления сделок в строки.

  class TesterDeal: public Deal
  {
    public:
      TesterDeal(const TradeDeal &td)
      {
        time = (datetime)td.time_create + TimeShift;
        price = td.price_open;
        string t = dealType(td.action);
        type = t == "buy" ? +1 : (t == "sell" ? -1 : 0);
        t = dealDir(td.entry);
        direction = 0;
        if(StringFind(t, "in") > -1) ++direction;
        if(StringFind(t, "out") > -1) --direction;
        volume = (double)td.volume;
        profit = td.profit;
        deal = (long)td.deal;
        order = (long)td.order;
        comment = td.comment[];
        symbol = td.symbol[];
        commission = td.commission;
        swap = td.storage;
      }
      
      static string dealType(const ENUM_DEAL_TYPE type)
      {
        return type == DEAL_TYPE_BUY ? "buy" : (type == DEAL_TYPE_SELL ? "sell" : "balance");
      }
      
      static string dealDir(const ENUM_DEAL_ENTRY entry)
      {
        string result = "";
        if(entry == DEAL_ENTRY_IN) result += "in";
        else if(entry == DEAL_ENTRY_OUT || entry == DEAL_ENTRY_OUT_BY) result += "out";
        else if(entry == DEAL_ENTRY_INOUT) result += "in out";
        return result;
      }
  };

Класс TesterReportAdapter помимо методов load и fillDealsArray содержит указатель на объект SINGLETESTERCACHE — главного класса библиотеки SingleTesterCache. Именно этот объект загружает по нашему требованию tst-файл и в случае успеха заполняет массив Deals, на основе которого работает наш метод fillDealsArray.

  template<typename T>
  class TesterReportAdapter: public BaseReportAdapter<T>
  {
    protected:
      SINGLETESTERCACHE *ptrSingleTesterCache;
      
      virtual bool fillDealsArray() override
      {
        for(int i = 0; i < ArraySize(ptrSingleTesterCache.Deals); i++)
        {
          if(TesterDeal::dealType(ptrSingleTesterCache.Deals[i].action) == "balance")
          {
            balance += ptrSingleTesterCache.Deals[i].profit;
          }
          else
          {
            array << new TesterDeal(ptrSingleTesterCache.Deals[i]);
          }
        }
        return true;
      }
      
    public:
      ~TesterReportAdapter()
      {
        if(CheckPointer(ptrSingleTesterCache) == POINTER_DYNAMIC) delete ptrSingleTesterCache;
      }
      
      virtual bool load(const string file) override
      {
        if(StringFind(file, ".tst") > 0)
        {
          // default cleanup
          BaseReportAdapter<T>::load(file);
          
          // specific cleanup
          if(CheckPointer(ptrSingleTesterCache) == POINTER_DYNAMIC) delete ptrSingleTesterCache;
          
          ptrSingleTesterCache = new SINGLETESTERCACHE();
          if(!ptrSingleTesterCache.Load(file))
          {
            delete ptrSingleTesterCache;
            ptrSingleTesterCache = NULL;
            return false;
          }
          size = generate();
          
          Print("Tester cache import: ", size, " trades from ", ArraySize(ptrSingleTesterCache.Deals), " deals");
        }
        return true;
      }
  };
  
  TesterReportAdapter<RECORD_CLASS> _defaultTSTReportAdapter;

В конце создается экземпляр адаптера по умолчанию для шаблонного типа RECORD_CLASS. Напомним, что в наш проект включен файл OLAPTradesCustom.mqh, в котором определен пользовательский класс записи CustomTradeRecord, и там же он обозначен директивой препроцессора макросом RECORD_CLASS. Таким образом, как только новый адаптер будет подключен в проект и пользователь укажет во входных параметрах tst-файл, адаптер начнет генерировать объекты класса CustomTradeRecord и для них автоматически будут рассчитаны наши "кастом"-поля MFE и MAE.

Посмотрим, как новый адаптер справляется со своими задачами. Вот пример кривых баланса в разбивке по символам из tst-файла.

Кривые балансов в разрезе символов

Кривые балансов в разрезе символов

Обратите внимание, что линии выводятся без прерываний, то есть наша реализация CGraphicInPlot::LinesPlot работает правильно. При работе с "прогрессивным" агрегатором (нарастающий итог), первым селектором всегда должен быть порядковый номер (или индекс) записей.

Отчеты оптимизации тестера как прикладная область анализа OLAP

Помимо файлов одиночных проходов тестера, компания MetaQuotes открыла также и формат opt-файлов с кэшем оптимизации. Для их чтения создана библиотека TesterCache (вполне логично, что автор тот же — fxsaber). На её основе легко создать прикладную прослойку для OLAP-анализа результатов оптимизации. Для этого необходимы: класс записи с полями, хранящими данные каждого прохода оптимизации, адаптер и, опционально, селекторы. У нас есть реализации этих компонентов для других прикладных областей, что позволяет использовать их в качестве руководства (плана). Затем добавим графический интерфейс (формально все готово и нужно поменять лишь настройки).

Создадим файл OLAPOpts.mqh аналогичный по назначению файлу OLAPTrades.mqh. Включим в него заголовочный файл TesterCache.mqh.

  #include <fxsaber/TesterCache/TesterCache.mqh>

Определим перечисление со всеми полями оптимизатора. Поля взяты из структуры ExpTradeSummary (она находится в файле fxsaber/TesterCache/ExpTradeSummary.mqh, файл автоматически подключается к библиотеке).

  enum OPT_CACHE_RECORD_FIELDS
  {
    FIELD_NONE,
    FIELD_INDEX,
    FIELD_PASS,
  
    FIELD_DEPOSIT,
    FIELD_WITHDRAWAL,
    FIELD_PROFIT,
    FIELD_GROSS_PROFIT,
    FIELD_GROSS_LOSS,
    FIELD_MAX_TRADE_PROFIT,
    FIELD_MAX_TRADE_LOSS,
    FIELD_LONGEST_SERIAL_PROFIT,
    FIELD_MAX_SERIAL_PROFIT,
    FIELD_LONGEST_SERIAL_LOSS,
    FIELD_MAX_SERIAL_LOSS,
    FIELD_MIN_BALANCE,
    FIELD_MAX_DRAWDOWN,
    FIELD_MAX_DRAWDOWN_PCT,
    FIELD_REL_DRAWDOWN,
    FIELD_REL_DRAWDOWN_PCT,
    FIELD_MIN_EQUITY,
    FIELD_MAX_DRAWDOWN_EQ,
    FIELD_MAX_DRAWDOWN_PCT_EQ,
    FIELD_REL_DRAWDOWN_EQ,
    FIELD_REL_DRAWDOWN_PCT_EQ,
    FIELD_EXPECTED_PAYOFF,
    FIELD_PROFIT_FACTOR,
    FIELD_RECOVERY_FACTOR,
    FIELD_SHARPE_RATIO,
    FIELD_MARGIN_LEVEL,
    FIELD_CUSTOM_FITNESS,
  
    FIELD_DEALS,
    FIELD_TRADES,
    FIELD_PROFIT_TRADES,
    FIELD_LOSS_TRADES,
    FIELD_LONG_TRADES,
    FIELD_SHORT_TRADES,
    FIELD_WIN_LONG_TRADES,
    FIELD_WIN_SHORT_TRADES,
    FIELD_LONGEST_WIN_CHAIN,
    FIELD_MAX_PROFIT_CHAIN,
    FIELD_LONGEST_LOSS_CHAIN,
    FIELD_MAX_LOSS_CHAIN,
    FIELD_AVERAGE_SERIAL_WIN_TRADES,
    FIELD_AVERAGE_SERIAL_LOSS_TRADES
  };
  
  #define OPT_CACHE_RECORD_FIELDS_LAST (FIELD_AVERAGE_SERIAL_LOSS_TRADES + 1)

Здесь есть все привычные показатели, такие как прибыль, просадка по балансу и эквити, количество торговых операций, коэффициент Шарпа и т.д. Единственное поле, которое мы добавили сами — это FIELD_INDEX: порядковые номер записи. Поля в структуре разных типов — long, double, int. У нас это все попадет в класс записи OptCacheRecord, унаследованный от Record и будет храниться в его массиве типа double.

С библиотекой мы будем общаться через специальную структуру OptCacheRecordInternal:

  struct OptCacheRecordInternal
  {
    ExpTradeSummary summary;
    MqlParam params[][5]; // [][name, current, low, step, high]
  };

Дело в том, что каждый проход тестера характеризуется не только показателями эффективности, но и связан с определенным набором входных параметров. В данной структуре входные параметры добавлены вслед за ExpTradeSummary в виде массива MqlParam. Имея эту структуру, довольно просто написать класс OptCacheRecord, заполняемый данными в формате оптимизатора.

  class OptCacheRecord: public Record
  {
    protected:
      static int counter; // number of passes
      
      void fillByTesterPass(const OptCacheRecordInternal &internal)
      {
        const ExpTradeSummary record = internal.summary;
        set(FIELD_INDEX, counter++);
        set(FIELD_PASS, record.Pass);
        set(FIELD_DEPOSIT, record.initial_deposit);
        set(FIELD_WITHDRAWAL, record.withdrawal);
        set(FIELD_PROFIT, record.profit);
        set(FIELD_GROSS_PROFIT, record.grossprofit);
        set(FIELD_GROSS_LOSS, record.grossloss);
        set(FIELD_MAX_TRADE_PROFIT, record.maxprofit);
        set(FIELD_MAX_TRADE_LOSS, record.minprofit);
        set(FIELD_LONGEST_SERIAL_PROFIT, record.conprofitmax);
        set(FIELD_MAX_SERIAL_PROFIT, record.maxconprofit);
        set(FIELD_LONGEST_SERIAL_LOSS, record.conlossmax);
        set(FIELD_MAX_SERIAL_LOSS, record.maxconloss);
        set(FIELD_MIN_BALANCE, record.balance_min);
        set(FIELD_MAX_DRAWDOWN, record.maxdrawdown);
        set(FIELD_MAX_DRAWDOWN_PCT, record.drawdownpercent);
        set(FIELD_REL_DRAWDOWN, record.reldrawdown);
        set(FIELD_REL_DRAWDOWN_PCT, record.reldrawdownpercent);
        set(FIELD_MIN_EQUITY, record.equity_min);
        set(FIELD_MAX_DRAWDOWN_EQ, record.maxdrawdown_e);
        set(FIELD_MAX_DRAWDOWN_PCT_EQ, record.drawdownpercent_e);
        set(FIELD_REL_DRAWDOWN_EQ, record.reldrawdown_e);
        set(FIELD_REL_DRAWDOWN_PCT_EQ, record.reldrawdownpercnt_e);
        set(FIELD_EXPECTED_PAYOFF, record.expected_payoff);
        set(FIELD_PROFIT_FACTOR, record.profit_factor);
        set(FIELD_RECOVERY_FACTOR, record.recovery_factor);
        set(FIELD_SHARPE_RATIO, record.sharpe_ratio);
        set(FIELD_MARGIN_LEVEL, record.margin_level);
        set(FIELD_CUSTOM_FITNESS, record.custom_fitness);
      
        set(FIELD_DEALS, record.deals);
        set(FIELD_TRADES, record.trades);
        set(FIELD_PROFIT_TRADES, record.profittrades);
        set(FIELD_LOSS_TRADES, record.losstrades);
        set(FIELD_LONG_TRADES, record.longtrades);
        set(FIELD_SHORT_TRADES, record.shorttrades);
        set(FIELD_WIN_LONG_TRADES, record.winlongtrades);
        set(FIELD_WIN_SHORT_TRADES, record.winshorttrades);
        set(FIELD_LONGEST_WIN_CHAIN, record.conprofitmax_trades);
        set(FIELD_MAX_PROFIT_CHAIN, record.maxconprofit_trades);
        set(FIELD_LONGEST_LOSS_CHAIN, record.conlossmax_trades);
        set(FIELD_MAX_LOSS_CHAIN, record.maxconloss_trades);
        set(FIELD_AVERAGE_SERIAL_WIN_TRADES, record.avgconwinners);
        set(FIELD_AVERAGE_SERIAL_LOSS_TRADES, record.avgconloosers);
        
        const int n = ArrayRange(internal.params, 0);
        for(int i = 0; i < n; i++)
        {
          set(OPT_CACHE_RECORD_FIELDS_LAST + i, internal.params[i][PARAM_VALUE].double_value);
        }
      }
    
    public:
      OptCacheRecord(const int customFields = 0): Record(OPT_CACHE_RECORD_FIELDS_LAST + customFields)
      {
      }
      
      OptCacheRecord(const OptCacheRecordInternal &record, const int customFields = 0): Record(OPT_CACHE_RECORD_FIELDS_LAST + customFields)
      {
        fillByTesterPass(record);
      }
      
      static int getRecordCount()
      {
        return counter;
      }
  
      static void reset()
      {
        counter = 0;
      }
  };
  
  static int OptCacheRecord::counter = 0;

В методе fillByTesterPass наглядно видно соответствие между элементами перечисления и полями ExpTradeSummary. Конструктор принимает в качестве параметра заполненную структуру OptCacheRecordInternal.

Посредником между библиотекой TesterCache и OLAP будет специализированный адаптер данных. Его задача — генерировать записи класса OptCacheRecord.

  template<typename T>
  class OptCacheDataAdapter: public DataAdapter
  {
    private:
      int size;
      int cursor;
      int paramCount;
      string paramNames[];
      TESTERCACHE<ExpTradeSummary> Cache;

Поле size — общее число записей, cursor — номер текущей записи при последовательном обходе кэша, paramCount — количество оптимизируемых параметров, а их имена хранятся в массиве paramNames. Переменная Cache типа TESTERCACHE<ExpTradeSummary> является рабочим объектом библиотеки TesterCache.

Начальная инициализация и чтение кэша оптимизации производится в методах reset, load и customize.

      void customize()
      {
        size = (int)Cache.Header.passes_passed;
        paramCount = (int)Cache.Header.opt_params_total;
        const int n = ArraySize(Cache.Inputs);
  
        ArrayResize(paramNames, n);
        int k = 0;
        
        for(int i = 0; i < n; i++)
        {
          if(Cache.Inputs[i].flag)
          {
            paramNames[k++] = Cache.Inputs[i].name[];
          }
        }
        if(k > 0)
        {
          ArrayResize(paramNames, k);
          Print("Optimized Parameters (", paramCount, " of ", n, "):");
          ArrayPrint(paramNames);
        }
      }
  
    public:
      OptCacheDataAdapter()
      {
        reset();
      }
      
      void load(const string optName)
      {
        if(Cache.Load(optName))
        {
          customize();
          reset();
        }
        else
        {
          cursor = -1;
        }
      }
      
      virtual void reset() override
      {
        cursor = 0;
        if(Cache.Header.version == 0) return;
        T::reset();
      }
      
      virtual int getFieldCount() const override
      {
        return OPT_CACHE_RECORD_FIELDS_LAST;
      }

Загрузка opt-файла осуществляется в методе load, где вызывается метод Cache.Load библиотеки, и в случае успеха из заголовка оптимизатора выделяются параметры эксперта (во вспомогательном методе customize). Метод reset просто сбрасывает текущий номер записи, который будет инкрементироваться при последующем обходе всех записей из ядра OLAP с помощью метода getNext. В последнем как раз и заполняется структура OptCacheRecordInternal данными из кэша оптимизации, после чего на её основе создается новая запись класса-параметра шаблона (T).

      virtual Record *getNext() override
      {
        if(cursor < size)
        {
          OptCacheRecordInternal internal;
          internal.summary = Cache[cursor];
          Cache.GetInputs(cursor, internal.params);
          cursor++;
          return new T(internal, paramCount);
        }
        return NULL;
      }
      ...
  };

Параметром шаблона, разумеется, является вышеописанный класс OptCacheRecord.

  #ifndef RECORD_CLASS
  #define RECORD_CLASS OptCacheRecord
  #endif
  
  OptCacheDataAdapter<RECORD_CLASS> _defaultOptCacheAdapter;

Он же определяется макросом как RECORD_CLASS, используемым в других частях OLAP ядра. Вот как выглядит диаграмма классов с учетом прежних поддерживаемых и новых адаптеров данных.

Диаграмма классов адаптеров данных

Диаграмма классов адаптеров данных

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

  enum OPT_CACHE_SELECTORS
  {
    SELECTOR_NONE,       // none
    SELECTOR_INDEX,      // ordinal number
    /* all the next require a field as parameter */
    SELECTOR_SCALAR,     // scalar(field)
    SELECTOR_QUANTS,     // quants(field)
    SELECTOR_FILTER      // filter(field)
  };

По сути, все поля записи относятся к одному из двух типов: показатели торговли (статистика) и параметры советника. Параметры имеет смысл организовывать в ячейки, точно соответствующие проверенным значениям. Например, если среди параметров есть период скользящего среднего и для него использовалось 10 значений, то в OLAP-кубе по этому параметру должно быть 10 ячеек. Это обеспечивает селектор квантизации (SELECTOR_QUANTS) с нулевым размером "корзины".

Для полей, являющихся показателями, имеет смысл делать разбивку на ячейки с некоторым шагом. Например, можно посмотреть распределение проходов по прибыли с шагом 100 "условных единиц". Но для этого опять подойдет селектор квантизации, правда на этот раз нужно задать размер "корзины" равным требуемому шагу. Прочие добавленные селекторы выполняют утилитарные функции. Так селектор порядкового номера (SELECTOR_INDEX) востребован для вычисления нарастающего итога, а скаляр (SELECTOR_SCALAR) позволяет получить одно число как характеристику всего среза.

Сами классы селекторов уже готовы и находятся в файле OLAPCommon.mqh.

Для выбранных типов селекторов напишем "фабричный" метод createSelector в шаблонной специализации класса движка OLAPEngine для оптимизации:

  class OLAPEngineOptCache: public OLAPEngine<OPT_CACHE_SELECTORS,OPT_CACHE_RECORD_FIELDS>
  {
    protected:
      virtual Selector<OPT_CACHE_RECORD_FIELDS> *createSelector(const OPT_CACHE_SELECTORS selector, const OPT_CACHE_RECORD_FIELDS field) override
      {
        const int standard = adapter.getFieldCount();
        switch(selector)
        {
          case SELECTOR_INDEX:
            return new SerialNumberSelector<OPT_CACHE_RECORD_FIELDS,OptCacheRecord>(FIELD_INDEX);
          case SELECTOR_SCALAR:
            return new OptCacheSelector(field);
          case SELECTOR_QUANTS:
            return field != FIELD_NONE ? new QuantizationSelector<OPT_CACHE_RECORD_FIELDS>(field, (int)field < standard ? quantGranularity : 0) : NULL;
        }
        return NULL;
      }
  
    public:
      OLAPEngineOptCache(): OLAPEngine() {}
      OLAPEngineOptCache(DataAdapter *ptr): OLAPEngine(ptr) {}
  };
  
  OLAPEngineOptCache _defaultEngine;

При создании селектора квантизации, в зависимости от того, является поле "стандартным" (то есть хранит стандартную статистику прохода тестера) или пользовательским (параметром эксперта), мы устанавливаем размер "корзины" равным переменной quantGranularity, либо нулю. Поле quantGranularity описано в базовом классе OLAPEngine, и мы можем его задавать как в конструкторе движка, так и впоследствии с помощью выделенного метода setQuant.

OptCacheSelector — простая обертка для BaseSelector<OPT_CACHE_RECORD_FIELDS>.

Графический интерфейс для анализа отчетов оптимизации тестера

Для визуализации результатов анализа оптимизации применим тот же интерфейс, что был в случае с торговыми отчетами. Фактически, мы можем скопировать файл OLAPGUI_Trade.mqh под новым именем OLAPGUI_Opts.mqh и внести в него небольшие коррективы. Очевидно, они касаются виртуальных методов setup и process.

  template<typename S, typename F>
  void OLAPDialog::setup() override
  {
    static const string _settings[ALGO_NUMBER][MAX_ALGO_CHOICES] =
    {
      // enum AGGREGATORS 1:1, default - sum
      {"sum", "average", "max", "min", "count", "profit factor", "progressive total", "identity", "variance"},
      // enum RECORD_FIELDS 1:1, default - profit amount
      {""},
      // enum SORT_BY, default - none
      {"none", "value ascending", "value descending", "label ascending", "label descending"},
      // enum ENUM_CURVE_TYPE partially, default - points
      {"points", "lines", "points/lines", "steps", "histogram"}
    };
    
    static const int _defaults[ALGO_NUMBER] = {0, FIELD_PROFIT, 0, 0};
  
    const int std = EnumSize<F,PackedEnum>(0);
    const int fields = std + customFieldCount;
  
    ArrayResize(settings, fields);
    ArrayResize(selectors, fields);
    selectors[0] = "(<selector>/field)"; // none
    selectors[1] = "<serial number>"; // the only selector, which can be chosen explicitly, it correspods to the 'index' field
  
    for(int i = 0; i < ALGO_NUMBER; i++)
    {
      if(i == 1) // pure fields
      {
        for(int j = 0; j < fields; j++)
        {
          settings[j][i] = j < std ? Record::legendFromEnum((F)j) : customFields[j - std];
        }
      }
      else
      {
        for(int j = 0; j < MAX_ALGO_CHOICES; j++)
        {
          settings[j][i] = _settings[i][j];
        }
      }
    }
  
    for(int j = 2; j < fields; j++) // 0-th is none
    {
      selectors[j] = j < std ? Record::legendFromEnum((F)j) : customFields[j - std];
    }
    
    ArrayCopy(defaults, _defaults);
  }

Здесь важно отметить, что между полями и селекторами практически нет различия, так как любое поле подразумевает селектор квантизации по этому же полю. Иными словами, селектор квантизации отвечает за всё. Ранее в проектах по отчетам и котировкам у нас были специальные селекторы под отдельные поля (такие как селектор прибыльности, селектор дня недели, селектор типа свечи и т.д.).

Названия всех элементов выпадающих списков с полями (они же селекторы по осям X, Y, Z) формируются из названий элементов перечисления OPT_CACHE_RECORD_FIELDS для стандартной статистики и из массива customFields для параметров экспертов. Ранее мы рассмотрели метод setCustomFields в базовом классе OLAPDialogBase, который позволяет заполнить массив customFields названиями из адаптера. А увязать их вместе мы сможем в коде аналитического эксперта OLAPGUI_Opts.mq5 (см. далее).

Стандартные поля выводятся в порядке элементов перечисления, а после них следуют поля, связанные с параметрами оптимизируемого эксперта, причем в том порядке, в каком они записаны в opt-файле.

Считывание состояния "контролов" и запуск процесса анализа производится в методе process.

  template<typename S, typename F>
  int OLAPDialog::process() override
  {
    SELECTORS Selectors[4];
    ENUM_FIELDS Fields[4];
    AGGREGATORS at = (AGGREGATORS)m_algo[0].Value();
    ENUM_FIELDS af = (ENUM_FIELDS)(AGGREGATORS)m_algo[1].Value();
    SORT_BY sb = (SORT_BY)m_algo[2].Value();
    
    if(at == AGGREGATOR_IDENTITY)
    {
      Print("Sorting is disabled for Identity");
      sb = SORT_BY_NONE;
    }
  
    ArrayInitialize(Selectors, SELECTOR_NONE);
    ArrayInitialize(Fields, FIELD_NONE);
  
    int matches[2] =
    {
      SELECTOR_NONE,
      SELECTOR_INDEX
    };
    
    for(int i = 0; i < AXES_NUMBER; i++)
    {
      if(!m_axis[i].IsVisible()) continue;
      int v = (int)m_axis[i].Value();
      if(v < 2) // selectors (which is specialized for a field already)
      {
        Selectors[i] = (SELECTORS)matches[v];
      }
      else // pure fields
      {
        Selectors[i] = at == AGGREGATOR_IDENTITY ? SELECTOR_SCALAR : SELECTOR_QUANTS;
        Fields[i] = (ENUM_FIELDS)(v);
      }
    }
    
    m_plot.CurvesRemoveAll();
  
    if(at == AGGREGATOR_IDENTITY) af = FIELD_NONE;
  
    m_plot.InitXAxis(at != AGGREGATOR_PROGRESSIVE ? new AxisCustomizer(m_plot.getGraphic(), false) : NULL);
    m_plot.InitYAxis(at == AGGREGATOR_IDENTITY ? new AxisCustomizer(m_plot.getGraphic(), true) : NULL);
  
    m_button_ok.Text("Processing...");
    return olapcore.process(Selectors, Fields, at, af, olapdisplay, sb);
  }

OLAP анализ и визуализация отчетов оптимизации

Штатный тестер MetaTrader позволяет анализировать результаты оптимизации многими способами, но всё же они ограничены стандартным набором. Созданный движок OLAP позволит пополнять этот инструментарий. Например, встроенная визуализация в режиме 2D всегда показывает максимальное значение прибыли для сочетания двух параметров советника, а параметров обычно больше. Это означает, что в каждой точке поверхности мы видим результаты для разных сочетаний прочих параметров, не попавших на оси. В результате, может сложиться слишком оптимистичная оценка прибыльности конкретных значений отображаемых параметров. Для более взвешенной оценки имело бы смысл смотреть среднее значение прибыли и её разброс. Это, и многое другое, мы и сможем сделать с помощью OLAP.

Выполнять OLAP-анализ отчетов оптимизации поручим новому неторгующему эксперту OLAPGUI_Opts.mq5. Его структура полностью повторяет OLAPGUI.mq5 и он даже проще, потому что не нужно подключать различные адаптеры в зависимости от типа заданного файла. Для результатов оптимизации это всегда будет opt-файл.

Во входных параметрах укажем название файла для анализа и размер шага квантизации для статистических параметров.

  input string OptFileName = "Integrity.opt";
  input uint QuantGranularity = 0;

Следует отметить, что для каждого поля желательно иметь свой собственный шаг квантизации, но пока он задается лишь один и меняется не из графического интерфейса. Это — одно из направлений для дальнейшего совершенствования проекта. Сейчас нужно помнить, что величина шага может подходить для одного поля и не подходить для другого (быть слишком большой или слишком маленькой). Следовательно, при необходимости нужно вызывать диалог свойств эксперта для смены кванта, прежде чем выбирать поле в выпадающем списке интерфейса OLAP.

После включения заголовочных файлов со всеми классами создаем экземпляр диалога и привязываем его к движку OLAP.

  #include <OLAP/OLAPOpts.mqh>
  #include <OLAP/GUI/OLAPGUI_Opts.mqh>
  
  OLAPDialog<SELECTORS,ENUM_FIELDS> dialog(_defaultEngine);

В обработчике OnInit подключаем новый адаптер к движку и инициируем загрузку данных из файла.

  int OnInit()
  {
    _defaultEngine.setAdapter(&_defaultOptCacheAdapter);
    _defaultEngine.setShortTitles(true);
    _defaultEngine.setQuant(QuantGranularity);
    _defaultOptCacheAdapter.load(OptFileName);
    dialog.setCustomFields(_defaultOptCacheAdapter);
  
    if(!dialog.Create(0, "OLAPGUI" + (OptFileName != "" ? " : " + OptFileName : ""), 0,  0, 0, 750, 560)) return INIT_FAILED;
    if(!dialog.Run()) return INIT_FAILED;
    
    return INIT_SUCCEEDED;
  }

Попробуем построить несколько аналитических разрезов для файла Integrity.opt и QuantGranularity = 100. В ходе оптимизации выбирались три параметра PricePeriod, Momentum, Sigma.

Вот как выглядит средняя прибыль в разбивке по значениям параметра PricePeriod.

Средняя прибыль в зависимости от значения параметра эксперта

Средняя прибыль в зависимости от значения параметра эксперта

Само по себе это мало что говорит без дисперсии.

Дисперсия прибыли в зависимости от значения параметра эксперта

Дисперсия прибыли в зависимости от значения параметра эксперта

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

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

Прибыльность стратегии (профит-фактор) в зависимости от значения параметра эксперта

Прибыльность стратегии (профит-фактор) в зависимости от значения параметра эксперта

Еще более хитрый взгляд на вещи предлагает средний размер периода в разбивке по уровням прибыли с шагом 100 (который мы задали во входном параметре QuantGranularity).

Среднее значение параметра для получения прибыли в различных диапазонах (с шагом 100 уе)

Среднее значение параметра для получения прибыли в различных диапазонах (с шагом 100 уе)

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

Прибыль vs значение параметра для всех позиций

Прибыль vs значение параметра для всех позиций

Разбивка прибыли по двум параметрам Momentum и Sigma выглядит так.

Средняя прибыль в разрезе двух параметров

Средняя прибыль в разрезе двух параметров

Чтобы посмотреть общее распределение прибылей по уровням с шагом 100, выберем поле profit из статистики по оси X и агрегатор count.

Распределение прибылей по диапазонам с шагом 100 уе

Распределение прибылей всех проходов по диапазонам с шагом 100 уе

Наконец, оценить влияние количества трейдов на прибыль дает возможность агрегатор identity. В принципе, этот агрегатор позволяет наглядно увидеть многие другие закономерности.

Прибыль vs количество трейдов

Прибыль vs количество трейдов

Заключение

В данной статье мы расширили области применения MQL OLAP на отчеты тестера об одиночных проходах и оптимизации. Обновленная структура классов позволяет и в дальнейшем наращивать арсенал средств OLAP. Предложенная реализация, разумеется, не является исчерпывающей и может быть во многом улучшена (в частности, в плане 3D визуализации, включения в интерактивный GUI настроек фильтрации и квантизации по разным осям), но вместе с тем служит минимальным стартовым набором, на основе которого гораздо проще войти в мир OLAP. С его помощью трейдер способен обрабатывать большие объемы сырых данных и извлекать из них новые знания для принятия решений.

Прилагаемые файлы:

Experts

  • OLAPRPRT.mq5 — эксперт для анализа торговой истории счета, отчетов в форматах HTML и CSV (обновленный из статьи N3, без GUI);
  • OLAPQTS.mq5 — эксперт для анализа котировок (обновленный из статьи N3, без GUI);
  • OLAPGUI.mq5 — эксперт для анализа торговой истории счета, отчетов в форматах HTML и CSV, а также стандартных TST-файлов тестера (обновленный из статьи N2, GUI);
  • OLAPGUI_Opts.mq5 — эксперт для анализа результатов оптимизации из стандартных OPT-файлов тестера (новый, GUI);

Include

Ядро

  • OLAP/OLAPCommon.mqh — основной заголовочный файл с классами OLAP;
  • OLAP/OLAPTrades.mqh — стандартные классы для OLAP-анализа торговой истории;
  • OLAP/OLAPTradesCustom.mqh — пользовательские классы для OLAP-анализа торговой истории;
  • OLAP/OLAPQuotes.mqh — классы для OLAP-анализа котировок;
  • OLAP/OLAPOpts.mqh — классы для OLAP-анализа результатов оптимизации экспертов;
  • OLAP/ReportCubeBase.mqh — базовые классы для OLAP-анализа торговой истории;
  • OLAP/HTMLcube.mqh — классы для OLAP-анализа торговой истории в формате HTML;
  • OLAP/CSVcube.mqh — классы для OLAP-анализа торговой истории в формате CSV;
  • OLAP/TSTcube.mqh — классы для OLAP-анализа торговой истории в формате TST;
  • OLAP/PairArray.mqh — класс массива пар [значение;название] с поддержкой всех вариантов сортировки;
  • OLAP/GroupReportInputs.mqh — группа входных параметров для анализа торговых отчетов;
  • MT4Bridge/MT4Orders.mqh — библиотека MT4orders для работы с ордерами в едином стиле в МТ4 и в МТ5;
  • MT4Bridge/MT4Time.mqh — вспомогательный заголовочный файл с реализацией функций работы с датами в стиле MT4;
  • Marketeer/IndexMap.mqh — вспомогательный заголовочный файл с реализацией массива с комбинированным доступом по ключу и индексу;
  • Marketeer/Converter.mqh — вспомогательный заголовочный файл с объединением для конвертации типов данных;
  • Marketeer/GroupSettings.mqh — вспомогательный заголовочный файл групповых настроек входных параметров;
  • Marketeer/WebDataExtractor.mqh — парсер HTML;
  • Marketeer/empty_strings.h — список пустых тегов HTML;
  • Marketeer/HTMLcolumns.mqh — определения индексов колонок в HTML-отчетах;
  • Marketeer/RubbArray.mqh — вспомогательный заголовочный файл с "резиновым" массивом;
  • Marketeer/CSVReader.mqh — парсер CSV;
  • Marketeer/CSVcolumns.mqh — определения индексов колонок в CSV-отчетах;

Графический интерфейс

  • OLAP/GUI/OLAPGUI.mqh — общая реализация интерактивного оконного интерфейса;
  • OLAP/GUI/OLAPGUI_Trades.mqh — специализации графического интерфейса для анализа торговых отчетов;
  • OLAP/GUI/OLAPGUI_Opts.mqh — специализации графического интерфейса для анализа результатов оптимизации;
  • Layouts/Box.mqh — контейнер элементов управления;
  • Layouts/ComboBoxResizable.mqh — элемент управления выпадающий список с возможностью динамического изменения размера;
  • Layouts/MaximizableAppDialog.mqh — окно диалога с возможностью динамического изменения размера;
  • PairPlot/Plot.mqh — элемент управления с деловой графикой, поддерживающий динамическое изменение размера;
  • Layouts/res/expand2.bmp — кнопка максимизации окна;
  • Layouts/res/size6.bmp — кнопка изменения размера;
  • Layouts/res/size10.bmp — кнопка изменения размера;

TypeToBytes

  • TypeToBytes.mqh

SingleTesterCache

  • fxsaber/SingleTesterCache/SingleTesterCache.mqh
  • fxsaber/SingleTesterCache/SingleTestCacheHeader.mqh
  • fxsaber/SingleTesterCache/String.mqh
  • fxsaber/SingleTesterCache/ExpTradeSummaryExt.mqh
  • fxsaber/SingleTesterCache/ExpTradeSummarySingle.mqh
  • fxsaber/SingleTesterCache/TradeDeal.mqh
  • fxsaber/SingleTesterCache/TradeOrder.mqh
  • fxsaber/SingleTesterCache/TesterPositionProfit.mqh
  • fxsaber/SingleTesterCache/TesterTradeState.mqh

TesterCache

  • fxsaber/TesterCache/TesterCache.mqh
  • fxsaber/TesterCache/TestCacheHeader.mqh
  • fxsaber/TesterCache/String.mqh
  • fxsaber/TesterCache/ExpTradeSummary.mqh
  • fxsaber/TesterCache/TestCacheInput.mqh
  • fxsaber/TesterCache/TestInputRange.mqh
  • fxsaber/TesterCache/Mathematics.mqh
  • fxsaber/TesterCache/TestCacheRecord.mqh
  • fxsaber/TesterCache/TestCacheSymbolRecord.mqh

Патч стандартной библиотеки

  • Controls/Dialog.mqh
  • Controls/ComboBox.mqh

Files

  • 518562.history.csv
  • Integrity.tst
  • Integrity.opt
Прикрепленные файлы |
MQLOLAP4.zip (357.59 KB)
fxsaber
fxsaber | 5 мар 2020 в 11:42
Stanislav Korotky:
Прошу уточнить вопрос. SingleTesterCache и TesterCache уже подключены в статье.

Хочу запустить EX5, и он автоматом подхватит последний opt/tst-файл (последнее выполненное соответствующее задание в Тестере) для анализа.

Stanislav Korotky
Stanislav Korotky | 5 мар 2020 в 12:10
fxsaber:

Хочу запустить EX5, и он автоматом подхватит последний opt/tst-файл (последнее выполненное соответствующее задание в Тестере) для анализа.

Вот вариант для отдельного прогона.

//+------------------------------------------------------------------+
//|                                                      TSTcube.mqh |
//|                                    Copyright (c) 2020, Marketeer |
//|                          https://www.mql5.com/en/users/marketeer |
//|               Online Analytical Processing of trading hypercubes |
//|                            https://www.mql5.com/ru/articles/6602 |
//|                            https://www.mql5.com/ru/articles/6603 |
//|                            https://www.mql5.com/ru/articles/7656 |
//|                                                   rev. 5.03.2020 |
//+------------------------------------------------------------------+

#include "ReportCubeBase.mqh"
#include <fxsaber/SingleTesterCache/SingleTesterCache.mqh>
#include <fxsaber/MultiTester/MTTester.mqh>                 // +


class TesterDeal: public Deal
{
  public:
    TesterDeal(const TradeDeal &td)
    {
      time = (datetime)td.time_create + TimeShift;
      price = td.price_open;
      string t = dealType(td.action);
      type = t == "buy" ? +1 : (t == "sell" ? -1 : 0);
      t = dealDir(td.entry);
      direction = 0;
      if(StringFind(t, "in") > -1) ++direction;
      if(StringFind(t, "out") > -1) --direction;
      volume = (double)td.volume;
      profit = td.profit;
      deal = (long)td.deal;
      order = (long)td.order;
      comment = td.comment[];
      symbol = td.symbol[];
      commission = td.commission;
      swap = td.storage;
      
      // balance - SingleTesterCache.Deals[i].reserve
    }

    static string dealType(const ENUM_DEAL_TYPE type)
    {
      return type == DEAL_TYPE_BUY ? "buy" : (type == DEAL_TYPE_SELL ? "sell" : "balance");
    }

    static string dealDir(const ENUM_DEAL_ENTRY entry)
    {
      string result = "";
      if(entry == DEAL_ENTRY_IN) result += "in";
      else if(entry == DEAL_ENTRY_OUT || entry == DEAL_ENTRY_OUT_BY) result += "out";
      else if(entry == DEAL_ENTRY_INOUT) result += "in out";
      return result;
    }
};

template<typename T>
class TesterReportAdapter: public BaseReportAdapter<T>
{
  protected:
    SINGLETESTERCACHE *ptrSingleTesterCache;

    virtual bool fillDealsArray() override
    {
      for(int i = 0; i < ArraySize(ptrSingleTesterCache.Deals); i++)
      {
        if(TesterDeal::dealType(ptrSingleTesterCache.Deals[i].action) == "balance")
        {
          balance += ptrSingleTesterCache.Deals[i].profit;
        }
        else
        {
          array << new TesterDeal(ptrSingleTesterCache.Deals[i]);
        }
      }
      return true;
    }

  public:
    ~TesterReportAdapter()
    {
      if(CheckPointer(ptrSingleTesterCache) == POINTER_DYNAMIC) delete ptrSingleTesterCache;
    }

    virtual bool load(const string file) override
    {
      if(StringFind(file, ".tst") > 0)
      {
        BaseReportAdapter<T>::load(file);

        if(CheckPointer(ptrSingleTesterCache) == POINTER_DYNAMIC) delete ptrSingleTesterCache;

        bool loaded = true;                                                // +
        ptrSingleTesterCache = new SINGLETESTERCACHE();
        if(file == "*.tst")                                                // +
        {                                                                  // +
          uchar Bytes2[];                                                  // +
          // Why MTTESTER::GetLastTstCacheFileName() is private?           // +
          // Print("Loading ", MTTESTER::GetLastTstCacheFileName());       // +
          if(MTTESTER::GetLastTstCache(Bytes2) != -1)                      // +
          {                                                                // +
            loaded = ptrSingleTesterCache.Load(Bytes2);                    // +
          }                                                                // +
        }
        else
        {
          loaded = ptrSingleTesterCache.Load(file);                        // *
        }
        
        if(!loaded)                                                        // *
        {
          delete ptrSingleTesterCache;
          ptrSingleTesterCache = NULL;
          return false;
        }
        size = generate();
        
        Print("Tester cache import: ", size, " trades from ", ArraySize(ptrSingleTesterCache.Deals), " deals");
      }
      return true;
    }
};

TesterReportAdapter<RECORD_CLASS> _defaultTSTReportAdapter;

Хотел вывести имя последнего tst-файла в лог, но соотв. метод приватный.

В настройках нужно выбрать файл "*.tst" (пустое имя по-прежнему запускает анализ онлайн истории счета).

Stanislav Korotky
Stanislav Korotky | 5 мар 2020 в 12:27

Для результатов оптимизации:

#include <fxsaber/MultiTester/MTTester.mqh>                 // +

...

template<typename T>
class OptCacheDataAdapter: public DataAdapter
{
  private:
    TESTERCACHE<ExpTradeSummary> Cache;
    ...
    
  public:
    OptCacheDataAdapter()
    {
      reset();
    }
    
    void load(const string optName)
    {
      bool loaded = true;                                       // +
      if(optName == "")                                         // +
      {                                                         // +
        uchar Bytes[];                                          // +
        // Why GetLastOptCacheFileName() is private?            // +
        // Print("Loading ", MTTESTER::GetLastOptCacheFileName()); // +
        MTTESTER::GetLastOptCache(Bytes);                       // +
        loaded = Cache.Load(Bytes);                             // +
      }
      else
      {
        loaded = Cache.Load(optName);                           // *
      }
      if(loaded)                                                // *
      {
        customize();
        reset();
      }
      else
      {
        cursor = -1;
      }
    }
...
Реter Konow
Реter Konow | 5 мар 2020 в 14:40

Статья понравилась, впрочем, как и все статьи о технологии OLAP. 

Лично мне не хватило полета мысли, - философских формулировок сути, дающих представление о всем подходе, - об охватываемом пространстве задач и потенциале. А потенциал огромный. Печалят множественные и малосущественные детали, на которые тратиться время читателя. Мол - там такая переменная, а тут такая... Понимаю, что это необходимо новичкам, копирующим решения, но все же... Никакая технология не является статичной и если сейчас применят Ваши классы и методы, то в будущем, кто то захочет все поменять под себя, и для него полезность статьи уменьшится пропорционально количеству частных сущностей. Больше пишите о подходе в целом, - о его настоящем и будущем. С такими походами нужно быть глобальнее.

Но, это мое субъективное мнение. Спасибо за статью.

fxsaber
fxsaber | 5 мар 2020 в 15:40
Stanislav Korotky:

Хотел вывести имя последнего tst-файла в лог, но соотв. метод приватный.

Не пришло в голову, что кому-нибудь это может понадобиться. Если есть еще замечания по private->public, скажите. Сделаю.

Работа с таймсериями в библиотеке DoEasy (Часть 36): Объект таймсерий всех используемых периодов символа Работа с таймсериями в библиотеке DoEasy (Часть 36): Объект таймсерий всех используемых периодов символа

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

Прогнозирование временных рядов (Часть 2): метод наименьших квадратов опорных векторов (LS-SVM) Прогнозирование временных рядов (Часть 2): метод наименьших квадратов опорных векторов (LS-SVM)

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

Мультивалютный мониторинг торговых сигналов (Часть 3): Внедряем алгоритмы поиска Мультивалютный мониторинг торговых сигналов (Часть 3): Внедряем алгоритмы поиска

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

Работа с таймсериями в библиотеке DoEasy (Часть 37): Коллекция таймсерий - база данных таймсерий по символам и периодам Работа с таймсериями в библиотеке DoEasy (Часть 37): Коллекция таймсерий - база данных таймсерий по символам и периодам

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