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

22 января 2020, 15:24
Stanislav Korotky
5
1 550

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

Введение

Напомним, что было реализовано в предыдущих статьях (для тех, кто их по тем или иным причинам пропустил, настоятельно рекомендуется ознакомиться). Ядро находилось в файле OLAPcube.mqh, который содержал:

  • все основные классы селекторов и агрегаторов;
  • классы рабочих записей с исходными данными (абстрактный базовый Record и несколько специализированных наследников TradeRecord с данными о сделках);
  • базовый адаптер для чтения различных (абстрактных) источников данных и формирования из них массивов рабочих записей;
  • конкретный адаптер для торговой истории счета HistoryDataAdapter;
  • базовый класс отображения результатов и его простейшая реализация, использующая вывод в лог (Display, LogDisplay);
  • единая панель управления в виде класса Analyst, связывающего воедино адаптер, агрегатор и дисплей;

Специфические вещи, относящиеся к HTML-отчетам, были вынесены в файл HTMLcube.mqh, в котором, в частности, определены классы торговых сделок HTML-отчета HTMLTradeRecord и порождающий их адаптер HTMLReportAdapter.

Аналогичным образом в файл CSVcube.mqh были вынесены классы для торговых сделок из CSV-отчетов CSVTradeRecord и адаптер для них CSVReportAdapter.

Наконец, для упрощения интеграции OLAP с MQL5-программами был написан файл OLAPcore.mqh с классом-оберткой всего OLAP-функционала, применявшегося в демонстрационных проектах — OLAPWrapper.

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

Рефакторинг

На основе файла OLAPcube.mqh был сформирован новый — OLAPCommon.mqh, в котором остались лишь базовые типы. В числе изъятых оказались, прежде всего, перечисления, описывающие прикладной смысл полей данных, например, SELECTORS и TRADE_RECORD_FIELDS. Также были исключены связанные с торговлей классы селекторов и классы записей. Всё это было, разумеется, удалено не безвозвратно, а фактически перенесено в новый файл OLAPTrades.mqh, созданный для работы с торговой историей и отчетами.

Кроме того, в файл OLAPCommon.mqh переехал бывший класс-обертка OLAPWrapper, ставший теперь шаблонным и потому получивший новое название OLAPEngine. В качестве параметризующего параметра должны использоваться перечисления рабочих полей данных (например, для адаптации проектов из статей 1 и 2 это будет TRADE_RECORD_FIELDS, см. подробности далее).

Файл OLAPTrades.mqh содержит следующие типы (описаны в статьях 1 и 2):

  • перечисления TRADE_SELECTORS (бывшее SELECTORS), TRADE_RECORD_FIELDS;
  • селекторы TradeSelector, TypeSelector, SymbolSelector, MagicSelector, ProfitableSelector, DaysRangeSelector;
  • классы записей TradeRecord, CustomTradeRecord, HistoryTradeRecord;
  • адаптер HistoryDataAdapter;
  • движок OLAPEngineTrade — специализация OLAPEngine<TRADE_RECORD_FIELDS>;

Обратите внимание, что селектор DaysRangeSelector тоже здесь, то есть он стал штатным селектором анализа торговой истории, в то время как ранее находился в файле OLAPcore.mqh в качестве примера пользовательского селектора.

В конце файла создается по умолчанию экземпляр адаптера:

  HistoryDataAdapter<RECORD_CLASS> _defaultHistoryAdapter;

а также экземпляр OLAP-движка:

  OLAPEngineTrade _defaultEngine;

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

Файлы HTMLcube.mqh и CSVcube.mqh остались почти без изменений. Весь прежний функционал анализа торговой истории и отчетов сохранен, и для демонстрации этого к статье прилагается новый тестовый эксперт OLAPRPRT.mq5 — это аналог OLAPDEMO.mq5 из 1-ой статьи.

Используя файл OLAPTrades.mqh в качестве образца, легко создавать специализированные реализации классов OLAP для других типов данных.

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

Усовершенствование

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

Итак, были добавлены:

  • селектор MonthSelector;
  • селектор WorkWeekDaySelector;
  • агрегатор VarianceAggregator;

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

  template<typename E>
  class MonthSelector: public DateTimeSelector<E>
  {
    public:
      MonthSelector(const E f): DateTimeSelector(f, 12)
      {
        _typename = typename(this);
      }
      
      virtual bool select(const Record *r, int &index) const
      {
        double d = r.get(selector);
        datetime t = (datetime)d;
        index = TimeMonth(t) - 1;
        return true;
      }
      
      virtual string getLabel(const int index) const
      {
        static string months[12] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"};
        return months[index];
      }
  };

WorkWeekDaySelector является аналогом WeekDaySelector, но разбивает данные только по рабочим дням (от 1 до 5). Это очень удобно для анализа рынков, торговое расписания которых исключает выходные дни, поскольку показатели выходных дней всегда нулевые и под них не имеет смысл резервировать ячейки гиперкуба.

Агрегатор VarianceAggregator позволяет посчитать дисперсию данных и тем самым дополняет агрегатор среднего AverageAggregator. Суть нового агрегатора можно сравнить с величиной индикатора Average True Range (ATR), но в отличие от последнего, агрегатор можно рассчитать для любых разрезов (например, отдельно по часам дня или по дням недели), а также для прочих источников данных (например, разброс доходов в истории торговли).

  template<typename E>
  class VarianceAggregator: public Aggregator<E>
  {
    protected:
      int counters[];
      double sumx[];
      double sumx2[];
      
    public:
      VarianceAggregator(const E f, const Selector<E> *&s[], const Filter<E> *&t[]): Aggregator(f, s, t)
      {
        _typename = typename(this);
      }
      
      virtual void setSelectorBounds(const int length = 0) override
      {
        Aggregator<E>::setSelectorBounds();
        ArrayResize(counters, ArraySize(totals));
        ArrayResize(sumx, ArraySize(totals));
        ArrayResize(sumx2, ArraySize(totals));
        ArrayInitialize(counters, 0);
        ArrayInitialize(sumx, 0);
        ArrayInitialize(sumx2, 0);
      }
  
      virtual void update(const int index, const double value) override
      {
        counters[index]++;
        sumx[index] += value;
        sumx2[index] += value * value;
        
        const int n = counters[index];
        const double variance = (sumx2[index] - sumx[index] * sumx[index] / n) / MathMax(n - 1, 1);
        totals[index] = MathSqrt(variance);
      }
  };

Рис.1 Диаграмма классов агрегаторов

Рис.1 Диаграмма классов агрегаторов

Селекторы QuantizationSelector и SerialNumberSelector сделаны производными от BaseSelector, вместо более специфического TradeSelector. Причем QuantizationSelector получил новый параметр конструктора, позволяющий задать гранулярность селектора. По умолчанию она равна нулю, что означает, что данные группируются по точному совпадению значения соответствующего поля (поле указывается в селекторе). Например, в предыдущей статье мы использовали квантизацию по размеру лота для получения отчета прибылей в разбивке по размеру лотов, и в качестве ячеек куба могли выступать лоты, вроде 0.01, 0.1 и прочие, встретившиеся в истории торгов. Иногда удобнее проводить квантизацию с заданным шагом (размером ячейки). Его то и позволяет указать новый параметр конструктора. В исходном коде ниже добавленные фрагменты помечены комментарием "+".

  template<typename T>
  class QuantizationSelector: public BaseSelector<T>
  {
    protected:
      Vocabulary<double> quants;
      uint cell;                 // +
  
    public:
      QuantizationSelector(const T field, const uint granularity = 0 /* + */): BaseSelector<T>(field), cell(granularity)
      {
        _typename = typename(this);
      }
  
      virtual void prepare(const Record *r) override
      {
        double value = r.get(selector);
        if(cell != 0) value = MathSign(value) * MathFloor(MathAbs(value) / cell) * cell; // +
        quants.add(value);
      }
      
      virtual bool select(const Record *r, int &index) const override
      {
        double value = r.get(selector);
        if(cell != 0) value = MathSign(value) * MathFloor(MathAbs(value) / cell) * cell; // +
        index = quants.get(value);
        return (index >= 0);
      }
      
      virtual int getRange() const override
      {
        return quants.size();
      }
      
      virtual string getLabel(const int index) const override
      {
        return (string)(float)quants[index];
      }
  };

Кроме того, в имеющиеся классы были внесены прочие усовершенствования. В частности, классы фильтров Filter и FilterRange теперь поддерживают сравнение по значению поля, а не только по индексу ячейки, куда это значение попадает. Это удобно с точки зрения пользователя, потому что индекс ячейки не всегда известен заранее. Новый режим работы включается, если селектор возвратил индекс, равный -1 (добавленные строки помечены комментариями с '+'):

  template<typename E>
  class Filter
  {
    protected:
      Selector<E> *selector;
      double filter;
      
    public:
      Filter(Selector<E> &s, const double value): selector(&s), filter(value)
      {
      }
      
      virtual bool matches(const Record *r) const
      {
        int index;
        if(selector.select(r, index))
        {
          if(index == -1)                                             // +
          {                                                           // +
            if(dynamic_cast<FilterSelector<E> *>(selector) != NULL)   // +
            {                                                         // +
              return r.get(selector.getField()) == filter;            // +
            }                                                         // +
          }                                                           // +
          else                                                        // +
          {                                                           // +
            if(index == (int)filter) return true;
          }                                                           // +
        }
        return false;
      }
      
      Selector<E> *getSelector() const
      {
        return selector;
      }
      
      virtual string getTitle() const
      {
        return selector.getTitle() + "[" + (string)filter + "]";
      }
  };

Разумеется, нам потребуется селектор, который умеет возвращать -1 в качестве индекса. Он так и называется — FilterSelector.

  template<typename T>
  class FilterSelector: public BaseSelector<T>
  {
    public:
      FilterSelector(const T field): BaseSelector(field)
      {
        _typename = typename(this);
      }
  
      virtual bool select(const Record *r, int &index) const override
      {
        index = -1;
        return true;
      }
      
      virtual int getRange() const override
      {
        return 0;
      }
      
      virtual double getMin() const override
      {
        return 0;
      }
      
      virtual double getMax() const override
      {
        return 0;
      }
      
      virtual string getLabel(const int index) const override
      {
        return EnumToString(selector);
      }
  };

Как видно, данный селектор для любой записи возвращает true как признак того, что запись следует обработать и -1 в качестве индекса. За счет этого фильтр сможет "понять", что пользователь просит "просеять" запись не по индексу, а по значению поля. Пример того, как это используется, мы рассмотрим ниже.

В довершении ко всему дисплей лога поддерживает теперь сортировку многомерного куба по значениям — ранее многомерные кубы нельзя было сортировать. Сортировка по меткам многомерного куба доступна лишь отчасти — она возможна только для тех селекторов, которые "умеют" унифицированно форматировать метки строками в лексикографическом порядке. В частности, новый селектор по рабочим дням предоставляет их метки в виде "1`Monday", "2`Tuesday", "3`Wednesday", "4`Thursday", "5`Friday", и наличие номера дня в первом символе обеспечивает правильную сортировку. В противном случае для правильной реализации потребуется вводить функции сравнения меток. Кроме того, для особых "последовательных" агрегаторов — IdentityAggregator, ProgressiveTotalAggregator — скорее всего потребуется устанавливать приоритеты сторон куба, поскольку в этих агрегаторах по оси X всегда идет порядковый номер записи, который может быть желательно использовать при сортировке в последнюю, а не первую очередь.

Это не все модификации старых исходных кодов — полную их суть можно понять, сравнив контекстно исходные коды.

Расширение OLAP на прикладную область котировок

На основе базовых классов из OLAPCommon.mqh и по аналогии с OLAPTrades.mqh создадим файл с классами, предназначенными для анализа котировок — OLAPQuotes.mqh. В нем прежде всего опишем такие типы, как:

  • перечисления QUOTE_SELECTORS, QUOTE_RECORD_FIELDS;
  • селекторы QuoteSelector, ShapeSelector;
  • классы записей QuotesRecord, CustomQuotesBaseRecord;
  • адаптер QuotesDataAdapter;
  • OLAPEngineQuotes — специализация OLAPEngine<QUOTE_RECORD_FIELDS>;

Перечисление QUOTE_SELECTORS определено следующим образом:

  enum QUOTE_SELECTORS
  {
    SELECTOR_NONE,       // none
    SELECTOR_SHAPE,      // type
    SELECTOR_INDEX,      // ordinal number
    /* below datetime field assumed */
    SELECTOR_MONTH,      // month-of-year
    SELECTOR_WEEKDAY,    // day-of-week
    SELECTOR_DAYHOUR,    // hour-of-day
    SELECTOR_HOURMINUTE, // minute-of-hour
    /* the next require a field as parameter */
    SELECTOR_SCALAR,     // scalar(field)
    SELECTOR_QUANTS,     // quants(field)
    SELECTOR_FILTER      // filter(field)
  };

Селектор формы (shape) дифференцирует бары по типам: бычий, медвежий и нейтральный в зависимости от направления движения цены.

Селектор индекса соответствует классу SerialNumberSelector, который определен в базовых классах (файл OLAPCommon.mqh). Если в случае торговых операций это были порядковые номера сделок, то для котировок это будут номера баров.

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

Поля данных в котировках описаны следующим перечислением:

  enum QUOTE_RECORD_FIELDS
  {
    FIELD_NONE,          // none
    FIELD_INDEX,         // index (bar number)
    FIELD_SHAPE,         // type (bearish/flat/bullish)
    FIELD_DATETIME,      // datetime
    FIELD_PRICE_OPEN,    // open price
    FIELD_PRICE_HIGH,    // high price
    FIELD_PRICE_LOW,     // low price
    FIELD_PRICE_CLOSE,   // close price
    FIELD_PRICE_RANGE_OC,// price range (OC)
    FIELD_PRICE_RANGE_HL,// price range (HL)
    FIELD_SPREAD,        // spread
    FIELD_TICK_VOLUME,   // tick volume
    FIELD_REAL_VOLUME,   // real volume
    FIELD_CUSTOM1,       // custom 1
    FIELD_CUSTOM2,       // custom 2
    FIELD_CUSTOM3,       // custom 3
    FIELD_CUSTOM4,       // custom 4
    QUOTE_RECORD_FIELDS_LAST
  };

Назначение каждого должно быть ясно из названий и комментариев.

Два вышеназванных перечисления вынесены в макросы:

  #define SELECTORS QUOTE_SELECTORS
  #define ENUM_FIELDS QUOTE_RECORD_FIELDS

Обратите внимание, что подобные макроопределения — SELECTORS и ENUM_FIELDS — имеются во всех "прикладных" заголовочных файлах — в нашем случае их пока два (OLAPTrades.mqh, OLAPQuotes.mqh — для истории торговых операций и котировок), но может быть и больше. Таким образом, в любом проекте, использующем OLAP, сейчас можно одновременно анализировать только одну прикладную область (включив, например, либо OLAPTrades.mqh, либо OLAPQuotes.mqh, но не то и другое сразу). Для того чтобы проводить перекрестный анализ разных кубов, потребуется сделать еще один небольшой рефакторинг. Это оставлено для самостоятельной проработки, поскольку задачи параллельного анализа нескольких метакубов представляются более специфическими и редкими.

Родительский селектор для котировок представляет собой специализацию BaseSelector с полями QUOTE_RECORD_FIELDS:

  class QuoteSelector: public BaseSelector<QUOTE_RECORD_FIELDS>
  {
    public:
      QuoteSelector(const QUOTE_RECORD_FIELDS field): BaseSelector(field)
      {
      }
  };

Селектор типа бара (бычий или медвежий) ShapeSelector реализован так:

  class ShapeSelector: public QuoteSelector
  {
    public:
      ShapeSelector(): QuoteSelector(FIELD_SHAPE)
      {
        _typename = typename(this);
      }
  
      virtual bool select(const Record *r, int &index) const
      {
        index = (int)r.get(selector);
        index += 1; // shift from -1, 0, +1 to [0..2]
        return index >= getMin() && index <= getMax();
      }
      
      virtual int getRange() const
      {
        return 3; // 0 through 2
      }
      
      virtual string getLabel(const int index) const
      {
        const static string types[3] = {"bearish", "flat", "bullish"};
        return types[index];
      }
  };

Зарезервировано 3 значения, обозначающих типы: -1 — движение цены вниз, 0 — флет, +1 — движение вверх. Индексы ячеек, соответственно, лежат в пределах от 0 до 2 (включительно). Заполнение поля актуальным значением типа конкретного бара приведено ниже, в классе QuotesRecord.

Рис.2 Диаграмма классов селекторов

Рис.2 Диаграмма классов селекторов

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

  class QuotesRecord: public Record
  {
    protected:
      static int counter; // number of bars
      
      void fillByQuotes(const MqlRates &rate)
      {
        set(FIELD_INDEX, counter++);
        set(FIELD_SHAPE, rate.close > rate.open ? +1 : (rate.close < rate.open ? -1 : 0));
        set(FIELD_DATETIME, (double)rate.time);
        set(FIELD_PRICE_OPEN, rate.open);
        set(FIELD_PRICE_HIGH, rate.high);
        set(FIELD_PRICE_LOW, rate.low);
        set(FIELD_PRICE_CLOSE, rate.close);
        set(FIELD_PRICE_RANGE_OC, (rate.close - rate.open) / _Point);
        set(FIELD_PRICE_RANGE_HL, (rate.high - rate.low) * MathSign(rate.close - rate.open) / _Point);
        set(FIELD_SPREAD, (double)rate.spread);
        set(FIELD_TICK_VOLUME, (double)rate.tick_volume);
        set(FIELD_REAL_VOLUME, (double)rate.real_volume);
      }
    
    public:
      QuotesRecord(): Record(QUOTE_RECORD_FIELDS_LAST)
      {
      }
      
      QuotesRecord(const MqlRates &rate): Record(QUOTE_RECORD_FIELDS_LAST)
      {
        fillByQuotes(rate);
      }
      
      static int getRecordCount()
      {
        return counter;
      }
  
      static void reset()
      {
        counter = 0;
      }
  
      virtual string legend(const int index) const override
      {
        if(index >= 0 && index < QUOTE_RECORD_FIELDS_LAST)
        {
          return EnumToString((QUOTE_RECORD_FIELDS)index);
        }
        return "unknown";
      }
  };

Вся информация поступает из структуры MqlRates. Создание экземпляров класса будет показано далее в реализации адаптера.

В этом же классе определено прикладное назначение полей (целое, вещественное, дата), что необходимо в связи с тем, что все поля записей чисто технически хранятся в массиве типа double.

  class QuotesRecord: public Record
  {
    protected:
      const static char datatypes[QUOTE_RECORD_FIELDS_LAST];
  
    public:
      ...
      static char datatype(const int index)
      {
        return datatypes[index];
      }
  };
  
  const static char QuotesRecord::datatypes[QUOTE_RECORD_FIELDS_LAST] =
  {
    0,   // none
    'i', // index, serial number
    'i', // type (-1 down/0/+1 up)
    't', // datetime
    'd', // open price
    'd', // high price
    'd', // low price
    'd', // close price
    'd', // range OC
    'd', // range HL
    'i', // spread
    'i', // tick
    'i', // real
    'd',    // custom 1
    'd',    // custom 2
    'd',    // custom 3
    'd'     // custom 4
  };

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

Для поддержки возможности заполнения кастом-полей имеется промежуточный класс — его основная цель вызвать fillCustomFields из пользовательского класса, указанного базовым с помощью шаблона (таким образом, в момент вызова конструктора CustomQuotesBaseRecord наш пользовательский объект уже создан и заполнен стандартными полями, которые часто нужны для расчета кастом-полей):

  template<typename T>
  class CustomQuotesBaseRecord: public T
  {
    public:
      CustomQuotesBaseRecord(const MqlRates &rate): T(rate)
      {
        fillCustomFields();
      }
  };

Он используется в адаптере котировок:

  template<typename T>
  class QuotesDataAdapter: public DataAdapter
  {
    private:
      int size;
      int cursor;
      
    public:
      QuotesDataAdapter()
      {
        reset();
      }
  
      virtual void reset() override
      {
        size = MathMin(Bars(_Symbol, _Period), TerminalInfoInteger(TERMINAL_MAXBARS));
        cursor = size - 1;
        T::reset();
      }
      
      virtual int reservedSize()
      {
        return size;
      }
      
      virtual Record *getNext()
      {
        if(cursor >= 0)
        {
          MqlRates rate[1];
          if(CopyRates(_Symbol, _Period, cursor, 1, rate) > 0)
          {
            cursor--;
            return new CustomQuotesBaseRecord<T>(rate[0]);
          }
          
          Print(__FILE__, " ", __LINE__, " ", GetLastError());
          
          return NULL;
        }
        return NULL;
      }
  };

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

Наконец, движок OLAP для котировок выглядит следующим образом:

  class OLAPEngineQuotes: public OLAPEngine<QUOTE_SELECTORS,QUOTE_RECORD_FIELDS>
  {
    protected:
      virtual Selector<QUOTE_RECORD_FIELDS> *createSelector(const QUOTE_SELECTORS selector, const QUOTE_RECORD_FIELDS field) override
      {
        switch(selector)
        {
          case SELECTOR_SHAPE:
            return new ShapeSelector();
          case SELECTOR_INDEX:
            return new SerialNumberSelector<QUOTE_RECORD_FIELDS,QuotesRecord>(FIELD_INDEX);
          case SELECTOR_MONTH:
            return new MonthSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME);
          case SELECTOR_WEEKDAY:
            return new WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME);
          case SELECTOR_DAYHOUR:
            return new DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME);
          case SELECTOR_HOURMINUTE:
            return new DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME);
          case SELECTOR_SCALAR:
            return field != FIELD_NONE ? new BaseSelector<QUOTE_RECORD_FIELDS>(field) : NULL;
          case SELECTOR_QUANTS:
            return field != FIELD_NONE ? new QuantizationSelector<QUOTE_RECORD_FIELDS>(field, QuantGranularity) : NULL;
          case SELECTOR_FILTER:
            return field != FIELD_NONE ? new FilterSelector<QUOTE_RECORD_FIELDS>(field) : NULL;
        }
        return NULL;
      }
  
      virtual void initialize() override
      {
        Print("Bars read: ", QuotesRecord::getRecordCount());
      }
  
    public:
      OLAPEngineQuotes(): OLAPEngine() {}
      OLAPEngineQuotes(DataAdapter *ptr): OLAPEngine(ptr) {}
    
  };

Все основные функции остались в базовом классе OLAPEngine, который был рассмотрен в первой статье, хоть и под другим названием OLAPWrapper. Здесь нам осталось лишь создать специфические для котировок селекторы.

Экземпляры адаптера по умолчанию и OLAP-движка по умолчанию предоставим в виде готовых объектов:

  QuotesDataAdapter<RECORD_CLASS> _defaultQuotesAdapter;
  OLAPEngineQuotes _defaultEngine;

Руководствуясь созданными классами для двух прикладных областей анализа (OLAPTrades.mqh, OLAPQuotes.mqh), легко расширять функционал OLAP на другие применения, такие как обработка результатов оптимизаций или данных, получаемых из внешних источников.

Рис.3 Диаграмма управляющих классов OLAP

Рис.3 Диаграмма управляющих классов OLAP

Эксперт для OLAP-анализа котировок

У нас все готово для использования созданных классов. Напишем неторгующий эксперт OLAPQTS.mq5. Его структура будет аналогична OLAPRPRT.mq5, с помощью которого анализируются торговые отчеты.

Для демонстрации вычисления/заполнения кастом-полей предусмотрен класс CustomQuotesRecord, унаследованный от QuotesRecord. Мы будем использовать некоторые кастом-поля для выявления в котировках закономерностей, пригодных для конструирования торговых стратегий. Все такие поля заполняются в методе fillCustomFields, но более подробно мы опишем их чуть ниже.

  class CustomQuotesRecord: public QuotesRecord
  {
    public:
      CustomQuotesRecord(): QuotesRecord() {}
      CustomQuotesRecord(const MqlRates &rate): QuotesRecord(rate)
      {
      }
      
      virtual void fillCustomFields() override
      {
  
        // ...
        
      }
      
      virtual string legend(const int index) const override
      {
        // ...
        return QuotesRecord::legend(index);
      }
  };

Для того, чтобы адаптер "знал" о нашем классе записей CustomQuotesRecord и создавал его экземпляры, необходимо определить следующий макрос перед включением OLAPQuotes.mqh:

  // this line plugs our class into default adapter in OLAPQuotes.mqh
  #define RECORD_CLASS CustomQuotesRecord
  
  #include <OLAP/OLAPQuotes.mqh>

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

  sinput string X = "————— X axis —————"; // · X ·
  input SELECTORS SelectorX = DEFAULT_SELECTOR_TYPE; // · SelectorX
  input ENUM_FIELDS FieldX = DEFAULT_SELECTOR_FIELD /* field does matter only for some selectors */; // · FieldX
  
  sinput string Y = "————— Y axis —————"; // · Y ·
  input SELECTORS SelectorY = SELECTOR_NONE; // · SelectorY
  input ENUM_FIELDS FieldY = FIELD_NONE; // · FieldY
  
  sinput string Z = "————— Z axis —————"; // · Z ·
  input SELECTORS SelectorZ = SELECTOR_NONE; // · SelectorZ
  input ENUM_FIELDS FieldZ = FIELD_NONE; // · FieldZ
  
  sinput string F = "————— Filter —————"; // · F ·
  input SELECTORS _Filter1 = SELECTOR_NONE; // · Filter1
  input ENUM_FIELDS _Filter1Field = FIELD_NONE; // · Filter1Field
  input string _Filter1value1 = ""; // · Filter1value1
  input string _Filter1value2 = ""; // · Filter1value2
  
  sinput string A = "————— Aggregator —————"; // · A ·
  input AGGREGATORS _AggregatorType = DEFAULT_AGGREGATOR_TYPE; // · AggregatorType
  input ENUM_FIELDS _AggregatorField = DEFAULT_AGGREGATOR_FIELD; // · AggregatorField
  input SORT_BY _SortBy = SORT_BY_NONE; // · SortBy

Все селекторы и их поля сводятся в массивы для удобства последующей передачи в движок:

  SELECTORS _selectorArray[4];
  ENUM_FIELDS _selectorField[4];
  
  int OnInit()
  {
    _selectorArray[0] = SelectorX;
    _selectorArray[1] = SelectorY;
    _selectorArray[2] = SelectorZ;
    _selectorArray[3] = _Filter1;
    _selectorField[0] = FieldX;
    _selectorField[1] = FieldY;
    _selectorField[2] = FieldZ;
    _selectorField[3] = _Filter1Field;
    
    _defaultEngine.setAdapter(&_defaultQuotesAdapter);
  
    EventSetTimer(1);
    return INIT_SUCCEEDED;
  }

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

Непосредственно запуск обработки в OnTimer выглядит следующим образом:

  LogDisplay _display(11, _Digits);
  
  void OnTimer()
  {
    EventKillTimer();
    
    double Filter1value1 = 0, Filter1value2 = 0;
    if(CustomQuotesRecord::datatype(_Filter1Field) == 't')
    {
      Filter1value1 = (double)StringToTime(_Filter1value1);
      Filter1value2 = (double)StringToTime(_Filter1value2);
    }
    else
    {
      Filter1value1 = StringToDouble(_Filter1value1);
      Filter1value2 = StringToDouble(_Filter1value2);
    }
    
    _defaultQuotesAdapter.reset();
    _defaultEngine.process(_selectorArray, _selectorField,
          _AggregatorType, _AggregatorField,
          _display,
          _SortBy,
          Filter1value1, Filter1value2);
  }

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

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

Тестирование OLAP-анализа котировок

В целях ознакомления с созданным инструментарием проведем несколько простых исследований котировок.

Откроем график EURUSD D1 и набросим на него эксперт OLAPQTS. Оставим все параметры со значениями по умолчанию, что означает выбор селектора type по оси X и агрегатора COUNT. Изменим лишь настройки фильтра: в параметре Filter1 установим вариант "filter(field)", в Filter1Field — datetime, а в Filter1Value1 и Filter1Value2 — "2019.01.01" и "2020.01.01" соответственно. Тем самым мы ограничим диапазон расчета 2019 годом.

В результате запуска эксперта получим примерно такой результат:

  OLAPQTS (EURUSD,D1)	Bars read: 12626
  OLAPQTS (EURUSD,D1)	CountAggregator<QUOTE_RECORD_FIELDS> FIELD_NONE [3]
  OLAPQTS (EURUSD,D1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,D1)	Selectors: 1
  OLAPQTS (EURUSD,D1)	X: ShapeSelector(FIELD_SHAPE) [3]
  OLAPQTS (EURUSD,D1)	Processed records: 259
  OLAPQTS (EURUSD,D1)	  134.00000: bearish
  OLAPQTS (EURUSD,D1)	    0.00000: flat
  OLAPQTS (EURUSD,D1)	  125.00000: bullish

Из лога видно, что было проанализировано 12626 баров (вся доступная история EURUSD D1), но из них только 259 попали под условие фильтра, и из них 134 оказались медвежьими, а 125 — бычьими.

Если переключить таймфрейм на H1, получим оценку часовых баров:

  OLAPQTS (EURUSD,H1)	Bars read: 137574
  OLAPQTS (EURUSD,H1)	CountAggregator<QUOTE_RECORD_FIELDS> FIELD_NONE [3]
  OLAPQTS (EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,H1)	Selectors: 1
  OLAPQTS (EURUSD,H1)	X: ShapeSelector(FIELD_SHAPE) [3]
  OLAPQTS (EURUSD,H1)	Processed records: 6196
  OLAPQTS (EURUSD,H1)	 3051.00000: bearish
  OLAPQTS (EURUSD,H1)	   55.00000: flat
  OLAPQTS (EURUSD,H1)	 3090.00000: bullish

Далее попробуем проанализировать спреды. Одна из особенностей MetaTrader 5 заключается в том, что в структуре MqlRates и в окне данных почему-то решено хранить и показывать минимальный спред. С точки зрения проверки торговых идей данный подход может представлять опасность, т.к. дает слишком оптимистичные оценки прибыли. Более оправданным кажется вариант, если бы в истории были доступны не только минимальные, но и максимальные спреды. Конечно, при необходимости можно использовать историю тиков, но все же побаровый режим более экономичный. Попробуем оценить реальные спреды в разбивке по часам суток.

На том же графике EURUSD H1 сохраним прежний фильтр по 2019 году и введем следующие настройки эксперта. Селектор X — "hour-of-day", агрегатор — "AVERAGE", поле агрегатора — "spread". Вот пример результата:

  OLAPQTS (EURUSD,H1)	Bars read: 137574
  OLAPQTS (EURUSD,H1)	AverageAggregator<QUOTE_RECORD_FIELDS> FIELD_SPREAD [24]
  OLAPQTS (EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,H1)	Selectors: 1
  OLAPQTS (EURUSD,H1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,H1)	Processed records: 6196
  OLAPQTS (EURUSD,H1)	    4.71984: 00
  OLAPQTS (EURUSD,H1)	    3.19066: 01
  OLAPQTS (EURUSD,H1)	    3.72763: 02
  OLAPQTS (EURUSD,H1)	    4.19455: 03
  OLAPQTS (EURUSD,H1)	    4.38132: 04
  OLAPQTS (EURUSD,H1)	    4.28794: 05
  OLAPQTS (EURUSD,H1)	    3.93050: 06
  OLAPQTS (EURUSD,H1)	    4.01158: 07
  OLAPQTS (EURUSD,H1)	    4.39768: 08
  OLAPQTS (EURUSD,H1)	    4.68340: 09
  OLAPQTS (EURUSD,H1)	    4.68340: 10
  OLAPQTS (EURUSD,H1)	    4.64479: 11
  OLAPQTS (EURUSD,H1)	    4.57915: 12
  OLAPQTS (EURUSD,H1)	    4.62934: 13
  OLAPQTS (EURUSD,H1)	    4.64865: 14
  OLAPQTS (EURUSD,H1)	    4.61390: 15
  OLAPQTS (EURUSD,H1)	    4.62162: 16
  OLAPQTS (EURUSD,H1)	    4.50579: 17
  OLAPQTS (EURUSD,H1)	    4.56757: 18
  OLAPQTS (EURUSD,H1)	    4.61004: 19
  OLAPQTS (EURUSD,H1)	    4.59459: 20
  OLAPQTS (EURUSD,H1)	    4.67054: 21
  OLAPQTS (EURUSD,H1)	    4.50775: 22
  OLAPQTS (EURUSD,H1)	    3.57312: 23

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

  OLAPQTS (EURUSD,M1)	Bars read: 1000000
  OLAPQTS (EURUSD,M1)	AverageAggregator<QUOTE_RECORD_FIELDS> FIELD_SPREAD [24]
  OLAPQTS (EURUSD,M1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,M1)	Selectors: 1
  OLAPQTS (EURUSD,M1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,M1)	Processed records: 371475
  OLAPQTS (EURUSD,M1)	   14.05653: 00
  OLAPQTS (EURUSD,M1)	    6.63397: 01
  OLAPQTS (EURUSD,M1)	    6.00707: 02
  OLAPQTS (EURUSD,M1)	    5.72516: 03
  OLAPQTS (EURUSD,M1)	    5.72575: 04
  OLAPQTS (EURUSD,M1)	    5.77588: 05
  OLAPQTS (EURUSD,M1)	    5.82541: 06
  OLAPQTS (EURUSD,M1)	    5.82560: 07
  OLAPQTS (EURUSD,M1)	    5.77979: 08
  OLAPQTS (EURUSD,M1)	    5.44876: 09
  OLAPQTS (EURUSD,M1)	    5.32619: 10
  OLAPQTS (EURUSD,M1)	    5.32966: 11
  OLAPQTS (EURUSD,M1)	    5.32096: 12
  OLAPQTS (EURUSD,M1)	    5.32117: 13
  OLAPQTS (EURUSD,M1)	    5.29633: 14
  OLAPQTS (EURUSD,M1)	    5.21140: 15
  OLAPQTS (EURUSD,M1)	    5.17084: 16
  OLAPQTS (EURUSD,M1)	    5.12794: 17
  OLAPQTS (EURUSD,M1)	    5.27576: 18
  OLAPQTS (EURUSD,M1)	    5.48078: 19
  OLAPQTS (EURUSD,M1)	    5.60175: 20
  OLAPQTS (EURUSD,M1)	    5.70999: 21
  OLAPQTS (EURUSD,M1)	    5.87404: 22
  OLAPQTS (EURUSD,M1)	    6.94555: 23

Здесь картина более реалистичная: в некоторые часы средний минимальный спред увеличился в 2-3 раза. Но чтобы сделать анализ еще более строгим, мы можем запросить не среднее, а максимальное значение, использовав агрегатор "MAX". Несмотря на то, что полученные значения будут по-прежнему максимальные из _минимальных_, следует не забывать, что они строятся на минутных барах внутри каждого часа, и потому хорошо описывают условия входов и выходов при краткосрочной торговле.

  OLAPQTS (EURUSD,M1)	Bars read: 1000000
  OLAPQTS (EURUSD,M1)	MaxAggregator<QUOTE_RECORD_FIELDS> FIELD_SPREAD [24]
  OLAPQTS (EURUSD,M1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,M1)	Selectors: 1
  OLAPQTS (EURUSD,M1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,M1)	Processed records: 371475
  OLAPQTS (EURUSD,M1)	  157.00000: 00
  OLAPQTS (EURUSD,M1)	   31.00000: 01
  OLAPQTS (EURUSD,M1)	   12.00000: 02
  OLAPQTS (EURUSD,M1)	   12.00000: 03
  OLAPQTS (EURUSD,M1)	   13.00000: 04
  OLAPQTS (EURUSD,M1)	   11.00000: 05
  OLAPQTS (EURUSD,M1)	   12.00000: 06
  OLAPQTS (EURUSD,M1)	   12.00000: 07
  OLAPQTS (EURUSD,M1)	   11.00000: 08
  OLAPQTS (EURUSD,M1)	   11.00000: 09
  OLAPQTS (EURUSD,M1)	   12.00000: 10
  OLAPQTS (EURUSD,M1)	   13.00000: 11
  OLAPQTS (EURUSD,M1)	   12.00000: 12
  OLAPQTS (EURUSD,M1)	   13.00000: 13
  OLAPQTS (EURUSD,M1)	   12.00000: 14
  OLAPQTS (EURUSD,M1)	   14.00000: 15
  OLAPQTS (EURUSD,M1)	   16.00000: 16
  OLAPQTS (EURUSD,M1)	   14.00000: 17
  OLAPQTS (EURUSD,M1)	   15.00000: 18
  OLAPQTS (EURUSD,M1)	   21.00000: 19
  OLAPQTS (EURUSD,M1)	   17.00000: 20
  OLAPQTS (EURUSD,M1)	   25.00000: 21
  OLAPQTS (EURUSD,M1)	   31.00000: 22
  OLAPQTS (EURUSD,M1)	   70.00000: 23

Что называется, почувствуйте разницу: начинали мы со спреда в районе 4 пунктов, а завершили десятками и даже сотней в полночь.

Оценим разброс спреда и заодно проверим, как работает новый агрегатор, выбрав "DEVIATION".

  OLAPQTS (EURUSD,M1)	Bars read: 1000000
  OLAPQTS (EURUSD,M1)	VarianceAggregator<QUOTE_RECORD_FIELDS> FIELD_SPREAD [24]
  OLAPQTS (EURUSD,M1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,M1)	Selectors: 1
  OLAPQTS (EURUSD,M1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,M1)	Processed records: 371475
  OLAPQTS (EURUSD,M1)	    9.13767: 00
  OLAPQTS (EURUSD,M1)	    3.12974: 01
  OLAPQTS (EURUSD,M1)	    2.72293: 02
  OLAPQTS (EURUSD,M1)	    2.70965: 03
  OLAPQTS (EURUSD,M1)	    2.68758: 04
  OLAPQTS (EURUSD,M1)	    2.64350: 05
  OLAPQTS (EURUSD,M1)	    2.64158: 06
  OLAPQTS (EURUSD,M1)	    2.64934: 07
  OLAPQTS (EURUSD,M1)	    2.62854: 08
  OLAPQTS (EURUSD,M1)	    2.72117: 09
  OLAPQTS (EURUSD,M1)	    2.80259: 10
  OLAPQTS (EURUSD,M1)	    2.79681: 11
  OLAPQTS (EURUSD,M1)	    2.80850: 12
  OLAPQTS (EURUSD,M1)	    2.81435: 13
  OLAPQTS (EURUSD,M1)	    2.83489: 14
  OLAPQTS (EURUSD,M1)	    2.90745: 15
  OLAPQTS (EURUSD,M1)	    2.95804: 16
  OLAPQTS (EURUSD,M1)	    2.96799: 17
  OLAPQTS (EURUSD,M1)	    2.88021: 18
  OLAPQTS (EURUSD,M1)	    2.76605: 19
  OLAPQTS (EURUSD,M1)	    2.72036: 20
  OLAPQTS (EURUSD,M1)	    2.85615: 21
  OLAPQTS (EURUSD,M1)	    2.94224: 22
  OLAPQTS (EURUSD,M1)	    4.60560: 23

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

Проверим заполнение поля с диапазоном движения цен в баре, работу квантизации при заданном размере ячейки, а также сортировку.

Для этого переключимся обратно на EURUSD D1, оставим прежний фильтр за 2019 год. В параметрах укажем:

  • QuantGranularity=100 (5-значные пункты)
  • SelectorX=quants
  • FieldX=price range (OC)
  • Aggregator=COUNT
  • SortBy=value (descending)

Получим результат:

  OLAPQTS (EURUSD,D1)	Bars read: 12627
  OLAPQTS (EURUSD,D1)	CountAggregator<QUOTE_RECORD_FIELDS> FIELD_NONE [20]
  OLAPQTS (EURUSD,D1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,D1)	Selectors: 1
  OLAPQTS (EURUSD,D1)	X: QuantizationSelector<QUOTE_RECORD_FIELDS>(FIELD_PRICE_RANGE_OC) [20]
  OLAPQTS (EURUSD,D1)	Processed records: 259
  OLAPQTS (EURUSD,D1)	      [value]   [title]
  OLAPQTS (EURUSD,D1) [ 0] 72.00000 "0.0"    
  OLAPQTS (EURUSD,D1) [ 1] 27.00000 "100.0"  
  OLAPQTS (EURUSD,D1) [ 2] 24.00000 "-100.0" 
  OLAPQTS (EURUSD,D1) [ 3] 24.00000 "-200.0" 
  OLAPQTS (EURUSD,D1) [ 4] 21.00000 "200.0"  
  OLAPQTS (EURUSD,D1) [ 5] 17.00000 "-300.0" 
  OLAPQTS (EURUSD,D1) [ 6] 16.00000 "300.0"  
  OLAPQTS (EURUSD,D1) [ 7] 12.00000 "-400.0" 
  OLAPQTS (EURUSD,D1) [ 8]  8.00000 "500.0"  
  OLAPQTS (EURUSD,D1) [ 9]  8.00000 "400.0"  
  OLAPQTS (EURUSD,D1) [10]  6.00000 "-700.0" 
  OLAPQTS (EURUSD,D1) [11]  6.00000 "-500.0" 
  OLAPQTS (EURUSD,D1) [12]  6.00000 "700.0"  
  OLAPQTS (EURUSD,D1) [13]  4.00000 "-600.0" 
  OLAPQTS (EURUSD,D1) [14]  2.00000 "600.0"  
  OLAPQTS (EURUSD,D1) [15]  2.00000 "1000.0" 
  OLAPQTS (EURUSD,D1) [16]  1.00000 "-800.0" 
  OLAPQTS (EURUSD,D1) [17]  1.00000 "-1100.0"
  OLAPQTS (EURUSD,D1) [18]  1.00000 "900.0"  
  OLAPQTS (EURUSD,D1) [19]  1.00000 "-1000.0"

Как и следовало ожидать, наибольшее количество баров (72) попало в нулевой диапазон, т.е. изменение цены на них не превысило 100 пунктов. Изменения ±100 и ±200 пунктов — следующие по "популярности", и так далее.

Но все это пока лишь демонстрация работоспособности OLAP при анализе котировок. Пора переходить к этапу, когда OLAP позволит создавать торговые стратегии.

Конструирование торговых стратегий на основе OLAP-анализа котировок. Часть 1

Попробуем выяснить, нет ли в котировках закономерностей, связанных с внутридневными и внутринедельными циклами. Если в какие-то часы, в какие-то дни недели преобладающие движения цены в среднем несимметричны, мы можем воспользоваться этим для открытия сделок. Чтобы обнаружить такую цикличность, потребуется использовать селекторы "hour-of-day" и "day-of-week". Селекторы можно использовать последовательно по одному или применить сразу оба — каждый по своей оси. Второй вариант — предпочтительнее в том смысле, что позволит построить более точные разрезы данных, учитывающие сразу 2 фактора (цикла). Какой именно селектор ставить на ось X, а какой на Y — непринципиально для программы, но от этого меняются ("транспонируются") отображаемые пользователю результаты.

Поскольку диапазон индексов указанных селекторов составляет, соответственно, 24 (часов в сутках) и 5 (дней в рабочей неделе), размер куба получится равным 120. В принципе, у нас есть возможность подключить и сезонную цикличность внутри года, выбрав по оси Z селектор "month-of-year". Но для простоты ограничимся двумерным кубом.

Изменение цены внутри бара у нас представлено в двух полях: FIELD_PRICE_RANGE_OC и FIELD_PRICE_RANGE_HL. Первое дает разницу в пунктах между ценами Open и Close, второе — размах между High и Low. Воспользуемся первым из них как источником статистики по потенциальным сделкам. Осталось решить, какую именно статистику считать, то есть какой агрегатор применить.

Как ни странно, нам здесь может пригодиться агрегатор ProfitFactorAggregator. Напомним, что он раздельно суммирует положительные и отрицательные значения заданного поля у всех записей и затем возвращает их частное: делит положительное на отрицательное, взятое по модулю. Таким образом, если в какой-то ячейке гипер-куба будут преобладать положительные приращения цены, мы получим профит-фактор заметно выше 1, а если преобладать будут отрицательные приращения, то профит-фактор заметно ниже 1. Иными словами, все значения, сильно отличающиеся от 1, сигнализируют о хороших условиях открыть длинную или короткую сделку. Когда профит-фактор больше 1, прибыльны покупки, а когда профит-фактор меньше 1, прибыльны продажи. Реальный профит-фактор продаж — это обратная величина для расчетного значения.

Проведем анализ на EURUSD H1. Выберем входные параметры:

  • SelectorX=hour-of-day
  • SelectorY=day-of-week
  • Filter1=field
  • Filter1Field=datetime
  • Filter1Value1=2019.01.01
  • Filter1Value2=2020.01.01
  • AggregatorType=Profit Factor
  • AggregatorField=price range (OC)
  • SortBy=value (descending)

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

  OLAPQTS (EURUSD,H1)	Bars read: 137597
  OLAPQTS (EURUSD,H1)	ProfitFactorAggregator<QUOTE_RECORD_FIELDS> FIELD_PRICE_RANGE_OC [120]
  OLAPQTS (EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,H1)	Selectors: 2
  OLAPQTS (EURUSD,H1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,H1)	Y: WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [5]
  OLAPQTS (EURUSD,H1)	Processed records: 6196
  OLAPQTS (EURUSD,H1)	      [value]           [title]
  OLAPQTS (EURUSD,H1) [  0] 5.85417 "00; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  1] 5.79204 "00; 5`Friday"   
  OLAPQTS (EURUSD,H1) [  2] 5.25194 "00; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  3] 4.10104 "01; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  4] 4.00463 "01; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [  5] 2.93725 "01; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  6] 2.50000 "00; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  7] 2.44557 "15; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  8] 2.43496 "04; 5`Friday"   
  OLAPQTS (EURUSD,H1) [  9] 2.36278 "20; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [ 10] 2.33917 "04; 4`Thursday" 
  ...
  OLAPQTS (EURUSD,H1) [110] 0.49096 "09; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [111] 0.48241 "13; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [112] 0.45891 "19; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [113] 0.45807 "19; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [114] 0.44993 "14; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [115] 0.44513 "23; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [116] 0.42693 "23; 1`Monday"   
  OLAPQTS (EURUSD,H1) [117] 0.37026 "10; 1`Monday"   
  OLAPQTS (EURUSD,H1) [118] 0.34662 "23; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [119] 0.19705 "23; 5`Friday"   

Обратите внимание, что метки двух измерений X и Y — час и день недели — выводятся у каждого значения.

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

  virtual void fillCustomFields() override
  {
    const double newBarRange = get(FIELD_PRICE_RANGE_OC);
    const double spread = get(FIELD_SPREAD);

    set(FIELD_CUSTOM1, MathSign(newBarRange) * (MathAbs(newBarRange) - spread));
    set(FIELD_CUSTOM2, MathSign(newBarRange) * MathSign(MathAbs(newBarRange) - spread));
    
    // ...
  }

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

  OLAPQTS (EURUSD,H1)	Bars read: 137598
  OLAPQTS (EURUSD,H1)	ProfitFactorAggregator<QUOTE_RECORD_FIELDS> FIELD_CUSTOM1 [120]
  OLAPQTS (EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1546300800.0 ... 1577836800.0];
  OLAPQTS (EURUSD,H1)	Selectors: 2
  OLAPQTS (EURUSD,H1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,H1)	Y: WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [5]
  OLAPQTS (EURUSD,H1)	Processed records: 6196
  OLAPQTS (EURUSD,H1)	      [value]           [title]
  OLAPQTS (EURUSD,H1) [  0] 6.34239 "00; 5`Friday"   
  OLAPQTS (EURUSD,H1) [  1] 5.63981 "00; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  2] 5.15044 "00; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  3] 4.41176 "01; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [  4] 4.18052 "01; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  5] 3.04167 "01; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  6] 2.60000 "00; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  7] 2.53118 "15; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  8] 2.50118 "04; 5`Friday"   
  OLAPQTS (EURUSD,H1) [  9] 2.47716 "04; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [ 10] 2.46208 "20; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [ 11] 2.20858 "03; 5`Friday"   
  OLAPQTS (EURUSD,H1) [ 12] 2.11964 "03; 1`Monday"   
  OLAPQTS (EURUSD,H1) [ 13] 2.11123 "19; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [ 14] 2.10998 "01; 1`Monday"   
  OLAPQTS (EURUSD,H1) [ 15] 2.07638 "10; 4`Thursday"
  OLAPQTS (EURUSD,H1) [ 16] 1.95498 "09; 5`Friday"    
  ...
  OLAPQTS (EURUSD,H1) [105] 0.59029 "11; 5`Friday"   
  OLAPQTS (EURUSD,H1) [106] 0.55008 "14; 5`Friday"   
  OLAPQTS (EURUSD,H1) [107] 0.54643 "13; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [108] 0.50484 "09; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [109] 0.50000 "22; 1`Monday"   
  OLAPQTS (EURUSD,H1) [110] 0.49744 "06; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [111] 0.46686 "13; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [112] 0.44753 "19; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [113] 0.44499 "19; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [114] 0.43838 "14; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [115] 0.41290 "23; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [116] 0.39770 "23; 1`Monday"   
  OLAPQTS (EURUSD,H1) [117] 0.35586 "10; 1`Monday"   
  OLAPQTS (EURUSD,H1) [118] 0.34721 "23; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [119] 0.18769 "23; 5`Friday"   

Они означают, что прибыль должны приносить торговые операции, в частности, в четверг: покупки в 0, 1 и 4 часа ночи, а продажи — в 19 и 23 часа. В пятницу же рекомендуется покупать в 0, 3, 4, 9 часов утра, а продавать в 11, 14 и 23. Правда, продажи в 23 часа пятницы могут быть рискованными из-за скорого закрытия торгов и потенциального гэпа в неблагоприятную сторону (но, кстати говоря, анализ гэпа также можно легко автоматизировать с помощью кастом-полей). Здесь и далее приемлемым уровнем профит-фактора считается 2 и более (для продажи, соответственно, 0.5 и менее). На практике показатели обычно хуже, чем теоретические, поэтому нужно оставить некоторый запас прочности.

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

Вообще говоря, мы не обязаны проводить анализ на том же таймфрейме, который выбран в младшем селекторе по полю дата. В частности, сейчас мы использовали "hour-of-day" на таймфрейме H1. Допускается брать любой таймфрейм, меньший или равный младшему селектору по полю даты. Например, мы можем провести аналогичный анализ на M15, сохранив группировку по часам с помощью селектора "hour-of-day". Тогда мы найдем профит-фактор для пятнадцатиминутных баров. Правда, для нашей текущей стратегии в этом случае потребуется дополнительно уточнить момент входа в рынок внутри часа, для чего можно было бы проанализировать, каковы наиболее вероятные способы формирования свечи на каждом часе (иными словами, после каких контрдвижений формируется основное тело бара). В исходном коде OLAPQTS в комментариях приведен пример "оцифровки" хвостов баров.

Один из других, причем более наглядных, способов выявить стабильные "покупные" и "продажные" бары в разбивке по часам и дням недели — использовать ProgressiveTotalAggregator. Для него нужно было бы задать селектор "ordinal number" (последовательный проход по всем барам) для оси X, селекторы "hour-of-day" и "day-of-week" для осей Y и Z, и прежнее поле для агрегирования — "custom 1". В результате мы получили бы фактически кривые балансов торговли по каждому специфическому часовом бару. Однако выводить такие данные в лог и анализировать там не очень удобно, поэтому данный способ больше подойдет, когда подключен графический "дисплей". Но это усложнило бы реализацию, так что мы пока обойдемся логом.

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

  input string BuyHours = "";
  input string SellHours = "";
  input uint ActiveDayOfWeek = 0;

Строковые параметры BuyHours и SellHours принимают списки часов, в которые следует открывать, соответственно, покупки и продажи. Часы в каждом списке разделяются запятыми. В параметре ActiveDayOfWeek задается день недели (значения от 1 — понедельник, до 5 — пятница). На стадии тестирования мы ограничимся проверкой гипотез для каждого дня отдельно, хотя в перспективе эксперт должен поддерживать расписание, совмещающее все дни недели. Если ActiveDayOfWeek установить в 0, то эксперт будет торговать во все дни по одному расписанию, но для этого нужно предварительно провести OLAP-анализ в разрезе единственного измерения "hour-of-day", сбросив "day-of-week" по оси Y — желающие могут проверить эту стратегию самостоятельно.

Настройки считываются и проверяются в OnInit:

  int buyHours[], sellHours[];
  
  int parseHours(const string &data, int &result[])
  {
    string str[];
    const int n = StringSplit(data, ',', str);
    ArrayResize(result, n);
    for(int i = 0; i < n; i++)
    {
      result[i] = (int)StringToInteger(str[i]);
    }
    return n;
  }
  
  int OnInit()
  {
    const int trend = parseHours(BuyHours, buyHours);
    const int reverse = parseHours(SellHours, sellHours);
    
    return trend > 0 || reverse > 0 ? INIT_SUCCEEDED : INIT_PARAMETERS_INCORRECT;
  }

В обработчике OnTick будем проверять списки торговых часов и устанавливать специальную переменную mode в +1, или -1, если текущий час найден в одном из них. Если час не найден нигде, mode будет равным 0, что означает закрытие имеющихся позиций (если они есть) и неоткрытие новых. Если ордеров еще нет, а mode не равно нулю, будет открыта новая позиция. Если позиция уже есть и совпадает по направлению с расписанием, она сохраняется. Если направления позиции и сигнала не совпадают, делается разворот. Одновременно может быть открыта только одна позиция.

  template<typename T>
  int ArrayFind(const T &array[], const T value)
  {
    const int n = ArraySize(array);
    for(int i = 0; i < n; i++)
    {
      if(array[i] == value) return i;
    }
    return -1;
  }
  
  void OnTick()
  {
    MqlTick tick;
    if(!SymbolInfoTick(_Symbol, tick)) return;
    
    const int h = TimeHour(TimeCurrent());
  
    int mode = 0;
    
    if(ArrayFind(buyHours, h) > -1)
    {
      mode = +1;
    }
    else
    if(ArrayFind(sellHours, h) > -1)
    {
      mode = -1;
    }
  
    if(ActiveDayOfWeek != 0 && ActiveDayOfWeek != _TimeDayOfWeek()) mode = 0; // skip all days except specified
  
    // pick up existing orders (if any)
    const int direction = CurrentOrderDirection();
    
    if(mode == 0)
    {
      if(direction != 0)
      {
        OrdersCloseAll();
      }
      return;
    }
    
    if(direction != 0) // there exist open orders
    {
      if(mode == direction) // keep direction
      {
        return; // existing trade goes on
      }
      OrdersCloseAll();
    }
    
    
    const int type = mode > 0 ? OP_BUY : OP_SELL;
    
    const double p = type == OP_BUY ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
      
    OrderSend(_Symbol, type, Lot, p, 100, 0, 0);
  }

Торговля ведется только по открытию баров — это заложено в торговой стратегии. Вспомогательные функции ArrayFind, CurrentOrderDirection, OrdersCloseAll представлены ниже. Все они, как и сам эксперт, используют библиотеку MT4Orders для упрощенной работы с торговым API. Также используется прилагаемый MT4Bridge/MT4Time.mqh для работы с датами.

  int CurrentOrderDirection(const string symbol = NULL)
  {
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
      if(OrderSelect(i, SELECT_BY_POS))
      {
        if(OrderType() <= OP_SELL && (symbol == NULL || symbol == OrderSymbol()))
        {
          return OrderType() == OP_BUY ? +1 : -1;
        }
      }
    }
    return 0;
  }
  
  void OrdersCloseAll(const string symbol = NULL, const int type = -1) // OP_BUY or OP_SELL
  {
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
      if(OrderSelect(i, SELECT_BY_POS))
      {
        if(OrderType() <= OP_SELL && (type == -1 || OrderType() == type) && (symbol == NULL || symbol == OrderSymbol()))
        {
          OrderClose(OrderTicket(), OrderLots(), OrderType() == OP_BUY ? SymbolInfoDouble(OrderSymbol(), SYMBOL_BID) : SymbolInfoDouble(OrderSymbol(), SYMBOL_ASK), 100);
        }
      }
    }
  }

Полный исходный код приложен к статье. Среди прочего, что опущено в тексте статьи для краткости, есть, например, теоретический расчет профит-фактора по той же логике, что и в OLAP-движке. Это позволяет сравнить его значение с практическим значением профит-фактора, выдаваемым тестером. Эти два значения обычно похожи, но не совпадают точно. Разумеется, теоретический профит-фактор имеет смысл только в том случае, когда в настройках эксперта выбрано расписание торговли в одну сторону — либо покупки (BuyHours), либо продажи (SellHours). Иначе два режима накладываются друг на друга, и теоретический ПФ стремится к значению возле 1. Кроме того, напомним, что прибыльный профит-фактор для продаж в теоретическом расчете обозначается величинами меньше 1, поскольку он представляет собой обратную величину к нормальному профит-фактору. Например, теоретический ПФ для продаж, равный 0.5, означает практический ПФ тестера, равный 2. Для режима покупок величины теоретического и практического ПФ имеют одинаковую трактовку: значения выше 1 — это прибыль, а меньше 1 — убыток.

Протестируем работу эксперта SingleBar в 2019 году на EURUSD H1. Установим найденные значения торговых часов для пятницы:

  • BuyHours=0,4,3,9
  • SellHours=23,14,11
  • ActiveDayOfWeek=5

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

Рис.4 Отчет эксперта SingleBar о торговле по найденному расписанию пятниц 2019 года, EURUSD H1

Рис.4 Отчет эксперта SingleBar о торговле по найденному расписанию пятниц 2019 года, EURUSD H1

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

Рис.5 Отчет эксперта SingleBar о торговле по расписанию пятниц 2019 года на интервале 2018-2019, EURUSD H1

Рис.5 Отчет эксперта SingleBar о торговле по расписанию пятниц 2019 года на интервале 2018-2019, EURUSD H1

Хотя показатели ухудшились, видно, что те же "правила" работали уже с середины 2018 года и потому могли бы быть найдены ранее с помощью OLAP-анализа для торговли "в текущем будущем". Однако поиск оптимального периода анализа и выяснение длительности действия найденных закономерностей — отдельная большая тема. В некотором смысле OLAP-анализ требует такой же оптимизации, как и оптимизация экспертов. Теоретически можно было бы реализовать подход, при котором OLAP встроен в эксперт, который в тестере прогоняется на различных участках истории со сдвигом начальных дат и различной длины, и потом для каждого вызывается тест на форвард-периоде. Эта технология известна в применении к обычным экспертам как кластерный пошаговый анализ (Cluster Walk-Forward), но MetaTrader её не поддерживает в полном объеме (на момент написания статьи возможен лишь автоматический запуск форвард-тестов, но сдвиг окна оптимизации и изменение его размеров приходится реализовывать самостоятельно либо на MQL5, либо сторонними средствами, вроде shell-скриптов).

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

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

Рис.6.а Отчет эксперта SingleBar о торговле по вторникам в 2018-2019 согласно анализу 2019 года, EURUSD H1

Рис.6.а Отчет эксперта SingleBar о торговле по вторникам в 2018-2019 согласно анализу 2019 года, EURUSD H1

Рис.6.b Отчет эксперта SingleBar о торговле по средам в 2018-2019 согласно анализу 2019 года, EURUSD H1

Рис.6.b Отчет эксперта SingleBar о торговле по средам в 2018-2019 согласно анализу 2019 года, EURUSD H1

Рис.6.c Отчет эксперта SingleBar о торговле по четвергам в 2018-2019 согласно анализу 2019 года, EURUSD H1

Рис.6.c Отчет эксперта SingleBar о торговле по четвергам в 2018-2019 согласно анализу 2019 года, EURUSD H1

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

Посмотрим, какие расписания торгов могли бы быть найдены, если анализировать котировки на более длительном периоде, например, с 2015 до 2019 года и затем торговать в форвард-режиме в 2019 году.

  OLAPQTS (EURUSD,H1)	Bars read: 137606
  OLAPQTS (EURUSD,H1)	ProfitFactorAggregator<QUOTE_RECORD_FIELDS> FIELD_CUSTOM3 [120]
  OLAPQTS (EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1420070400.0 ... 1546300800.0];
  OLAPQTS (EURUSD,H1)	Selectors: 2
  OLAPQTS (EURUSD,H1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,H1)	Y: WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [5]
  OLAPQTS (EURUSD,H1)	Processed records: 24832
  OLAPQTS (EURUSD,H1)	      [value]           [title]
  OLAPQTS (EURUSD,H1) [  0] 2.04053 "01; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  1] 1.78702 "01; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  2] 1.75055 "15; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  3] 1.71793 "00; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  4] 1.69210 "00; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  5] 1.64361 "04; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  6] 1.63956 "20; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  7] 1.62157 "05; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  8] 1.53032 "00; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  9] 1.49733 "16; 1`Monday"   
  OLAPQTS (EURUSD,H1) [ 10] 1.48539 "01; 5`Friday"   
  ...
  OLAPQTS (EURUSD,H1) [109] 0.74241 "16; 5`Friday"   
  OLAPQTS (EURUSD,H1) [110] 0.70346 "13; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [111] 0.68990 "23; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [112] 0.66238 "23; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [113] 0.66176 "14; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [114] 0.62968 "13; 1`Monday"   
  OLAPQTS (EURUSD,H1) [115] 0.62585 "23; 5`Friday"   
  OLAPQTS (EURUSD,H1) [116] 0.60150 "14; 5`Friday"   
  OLAPQTS (EURUSD,H1) [117] 0.55621 "11; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [118] 0.54919 "23; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [119] 0.49804 "11; 3`Wednesday"

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

  • BuyHours=1,4,20,5,0
  • SellHours=11,23,13
  • ActiveDayOfWeek=3

получим отчет:

Рис.7 Отчет эксперта SingleBar о торговле по средам с 2015 до 2020 согласно анализу, не включающему 2019 год, EURUSD H1

Рис.7 Отчет эксперта SingleBar о торговле по средам с 2015 до 2020 согласно анализу, не включающему 2019 год, EURUSD H1

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

Конструирование торговых стратегий на основе OLAP-анализа котировок. Часть 2

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

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

Руководствуясь значением этого поля, можно проверить 2 стратегии: трендовую и разворотную. Например, если использовать уже знакомый нам агрегатор профит-фактора по данному полю, то значения больше 1 обозначают прибыльность торговли в направлении предыдущего бара, а значения меньше 1 — прибыльность противоположного движения. По аналогии с предыдущими расчетами, чем более экстремально значение (сильно больше 1 или сильно меньше 1), тем выгоднее будут, соответственно, трендовые или разворотные операции по статистике.

В четвертом кастом-поле будем сохранять признак однонаправленности (+1) или разнонаправленности (-1) смежных баров. Оно позволит с помощью агрегаторов определить количество смежных разворотных баров и эффективность входов для трендовой и разворотной стратегий.

Поскольку бары гарантированно обходятся в хронологическом порядке (это обеспечивает адаптер), мы можем сохранять необходимые для расчетов размах предыдущего бара и его спред в статических переменных. Разумеется, это возможно до тех пор, пока предполагается использовать единственный экземпляр адаптера котировок (напомним, что его экземпляр по умолчанию создается в заголовочном файле). Это подходит для нашего примера и проще для восприятия, но в общем случае адаптер должен был бы передавать в конструктор кастом-записей (таких как, CustomQuotesBaseRecord) и далее в метод fillCustomFields некий контейнер, позволяющий сохранять и восстанавливать состояние, например, в виде ссылки на массив: fillCustomFields(double &bundle[]).

  class CustomQuotesRecord: public QuotesRecord
  {
    private:
      static double previousBarRange;
      static double previousSpread;
      
    public:
      // ...
      
      virtual void fillCustomFields() override
      {
        const double newBarRange = get(FIELD_PRICE_RANGE_OC);
        const double spread = get(FIELD_SPREAD);
  
        // ...
  
        if(MathAbs(previousBarRange) > previousSpread)
        {
          double mult = newBarRange * previousBarRange;
          double value = MathSign(mult) * MathAbs(newBarRange);
  
          // this is an attempt to approximate average losses due to spreads
          value += MathSignNonZero(value) * -1 * MathMax(spread, previousSpread);
          
          set(FIELD_CUSTOM3, value);
          set(FIELD_CUSTOM4, MathSign(mult));
        }
        else
        {
          set(FIELD_CUSTOM3, 0);
          set(FIELD_CUSTOM4, 0);
        }
  
        previousBarRange = newBarRange;
        previousSpread = spread;
      }
      
  };

Модифицируем значения входных параметров OLAPQTS. Самое главное изменение — выбор "custom 3" в AggregatorField. Селекторы по X и Y, тип агрегатора (ПФ) и сортировку оставим прежними. Также поменяем фильтр дат.

  • SelectorX=hour-of-day
  • SelectorY=day-of-week
  • Filter1=field
  • Filter1Field=datetime
  • Filter1Value1=2018.01.01
  • Filter1Value2=2019.01.01
  • AggregatorType=Profit Factor
  • AggregatorField=custom 3
  • SortBy=value (descending)

Как мы уже видели на примере анализа котировок с 2015 года, выбор длительного периода, скорее всего, оправдан только для систем, в которых ищется сезонная цикличность — ей соответствовал бы селектор month-of-year. В нашем же случае с селекторами часов и дней недели проведем анализ одного 2018 года и затем выполним форвард тест на 2019-м.

  OLAPQTS (EURUSD,H1)	Bars read: 137642
  OLAPQTS (EURUSD,H1)	Aggregator: ProfitFactorAggregator<QUOTE_RECORD_FIELDS> FIELD_CUSTOM3 [120]
  OLAPQTS (EURUSD,H1)	Filters: FilterRange::FilterSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME)[1514764800.0 ... 1546300800.0];
  OLAPQTS (EURUSD,H1)	Selectors: 2
  OLAPQTS (EURUSD,H1)	X: DayHourSelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [24]
  OLAPQTS (EURUSD,H1)	Y: WorkWeekDaySelector<QUOTE_RECORD_FIELDS>(FIELD_DATETIME) [5]
  OLAPQTS (EURUSD,H1)	Processed records: 6203
  OLAPQTS (EURUSD,H1)	      [value]           [title]
  OLAPQTS (EURUSD,H1) [  0] 2.65010 "23; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  1] 2.37966 "03; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  2] 2.33875 "04; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  3] 1.96317 "20; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  4] 1.91188 "18; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [  5] 1.89293 "23; 3`Wednesday"
  OLAPQTS (EURUSD,H1) [  6] 1.87159 "12; 1`Monday"   
  OLAPQTS (EURUSD,H1) [  7] 1.78903 "15; 5`Friday"   
  OLAPQTS (EURUSD,H1) [  8] 1.74461 "01; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [  9] 1.73821 "13; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [ 10] 1.73244 "14; 2`Tuesday"
  ...  
  OLAPQTS (EURUSD,H1) [110] 0.57331 "22; 4`Thursday" 
  OLAPQTS (EURUSD,H1) [111] 0.51515 "07; 5`Friday"   
  OLAPQTS (EURUSD,H1) [112] 0.50202 "05; 5`Friday"   
  OLAPQTS (EURUSD,H1) [113] 0.48557 "04; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [114] 0.46313 "23; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [115] 0.44182 "00; 2`Tuesday"  
  OLAPQTS (EURUSD,H1) [116] 0.40907 "13; 1`Monday"   
  OLAPQTS (EURUSD,H1) [117] 0.38230 "10; 1`Monday"   
  OLAPQTS (EURUSD,H1) [118] 0.36296 "22; 5`Friday"   
  OLAPQTS (EURUSD,H1) [119] 0.29462 "17; 5`Friday"   

Для проверки стратегии, заложенной в поле "custom 3", напишем торговый эксперт NextBar. Он позволит проверить найденные торговые возможности в тестере. Общее построение эксперта похоже на SingleBar: используются такие же входные параметры, те же функции и фрагменты кода. Правда, слегка усложняется торговая логика — с ней можно ознакомиться в прилагаемом исходном файле.

Выберем наиболее привлекательные комбинации часов (с ПФ примерно 2 и выше, или 0.5 и ниже), например, для понедельника:

  • PositiveHours=23,3
  • NegativeHours=10,13
  • ActiveDayOfWeek=1

Запустим тест на диапазоне 2018.01.01-2019.05.01:

Рис.8 Отчет эксперта NextBar на интервале 01.01.2018-01.05.2019 после OLAP-анализа 2018 года, EURUSD H1

Рис.8 Отчет эксперта NextBar на интервале 01.01.2018-01.05.2019 после OLAP-анализа 2018 года, EURUSD H1

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

Адаптивная торговля на основе OLAP-анализа котировок

До сих пор мы использовали для OLAP-анализа специальный неторгующий эксперт OLAPQTS, а для проверки гипотез разрабатывались отдельные торговые эксперты. Более логичным и удобным было бы иметь OLAP-движок, встроенный в торговый эксперт. Таким образом, робот мог бы автоматически анализировать котировки с заданной периодичностью и менять расписание торгов. Кроме того, выведя основные параметры анализа в настройки эксперта, мы можем проводить для них оптимизацию, которая в некотором смысле будет эмулировать упомянутую ранее технологию Walk-Forward. Назовем эксперт OLAPQRWF, как сокращение от OLAP of Quotes with Rolling Walk-Forward.

Основные входные параметры:

  input int BarNumberLookBack = 2880; // BarNumberLookBack (week: 120 H1, month: 480 H1, year: 5760 H1)
  input double Threshold = 2.0; // Threshold (PF >= Threshold && PF <= 1/Threshold)
  input int Strategy = 0; // Strategy (0 - single bar, 1 - adjacent bars)
  • BarNumberLookBack определят количество исторических баров, на которых будет проводиться OLAP-анализ (предполагается использование таймфрейма H1).
  • Threshold — порог сигнала по профит-фактору, который будет считаться достаточным для открытия сделок.
  • Strategy — номер проверяемой стратегии (сейчас у нас только две: 0 — статистика направления отдельных баров, 1 — статистика направлений двух смежных баров).

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

  enum UPDATEPERIOD
  {
    monthly,
    weekly
  };
  
  input UPDATEPERIOD Update = monthly;

Плюс ко всему мы можем выбирать не только стратегию, но и кастом-поля, на которых считается агрегатор. Напомним, что поля 1 и 3 считаются с учетом размаха баров (соответственно, для стратегий 0 и 1), а поля 2 и 4 учитывают только количество баров в каждом направлении.

  enum CUSTOMFIELD
  {
    range,
    count
  };
  
  input CUSTOMFIELD CustomField = range;

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

  const SELECTORS SelectorX = SELECTOR_DAYHOUR;
  const ENUM_FIELDS FieldX = FIELD_DATETIME;
  
  const SELECTORS SelectorY = SELECTOR_WEEKDAY;
  const ENUM_FIELDS FieldY = FIELD_DATETIME;
  
  const SELECTORS SelectorZ = SELECTOR_NONE;
  const ENUM_FIELDS FieldZ = FIELD_NONE;
  
  const SELECTORS _Filter1 = SELECTOR_FILTER;
  const ENUM_FIELDS _Filter1Field = FIELD_INDEX;
        int _Filter1value1 = -1; // to be filled with index of first bar to process
  const int _Filter1value2 = -1;
  
  const AGGREGATORS _AggregatorType = AGGREGATOR_PROFITFACTOR;
        ENUM_FIELDS _AggregatorField = FIELD_CUSTOM1;
  const SORT_BY _SortBy = SORT_BY_NONE;

Обратите внимание, что фильтровать бары мы будем не по времени, а по количеству с помощью FIELD_INDEX. Актуальное значение для _Filter1value1 будет высчитываться как разница между общим числом баров и заданным BarNumberLookBack. Тем самым всегда будут анализироваться последние BarNumberLookBack баров.

Эксперт будет торговать в побаровом режиме из обработчика OnTick.

  bool freshStart = true;
  
  void OnTick()
  {
    if(!isNewBar()) return;
    
    if(Bars(_Symbol, _Period) < BarNumberLookBack) return;
    
    const int m0 = TimeMonth(iTime(_Symbol, _Period, 0));
    const int w0 = _TimeDayOfWeek();
    const int m1 = TimeMonth(iTime(_Symbol, _Period, 1));
    const int w1 = _TimeDayOfWeek();
    
    static bool success = false;
    
    if((Update == monthly && m0 != m1)
    || (Update == weekly && w0 < w1)
    || freshStart)
    {
      success = calcolap();
      freshStart = !success;
    }
  
    //...
  }

В зависимости от периодичности анализа дожидаемся либо смены месяца, либо недели и запускаем OLAP в функции calcolap.

  bool calcolap()
  {
    _Filter1value1 = Bars(_Symbol, _Period) - BarNumberLookBack;
    _AggregatorField = Strategy == 0 ? (ENUM_FIELDS)(FIELD_CUSTOM1 + CustomField) : (ENUM_FIELDS)(FIELD_CUSTOM3 + CustomField);
  
    _defaultQuotesAdapter.reset();
    const int processed =
    _defaultEngine.process(_selectorArray, _selectorField,
          _AggregatorType, _AggregatorField,
          stats,                              // custom display object
          _SortBy,
          _Filter1value1, _Filter1value2);
    
    return processed == BarNumberLookBack;
  }

Эта часть кода нам также знакома. Пара изменений относится только к выборе поля агрегирования согласно входным параметрам, а также установке индекса первого анализируемого бара.

Еще одно важное изменение — использование своего специального объекта дисплея (stats), который будет вызываться движком OLAP после проведения анализа.

  class MyOLAPStats: public Display
  {
    // ...
    public:
      virtual void display(MetaCube *cube, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override
      {
        // ...
      }
  
      void trade(const double threshold, const double lots, const int strategy = 0)
      {
        // ...
      }
  };
  
  MyOLAPStats stats;

Поскольку данный объект будет извлекать из полученной статистики лучшие часы торговли, то и саму торговлю удобно доверить ему же, для чего зарезервирован метод trade. Таким образом, в OnTick можно дописать:

  void OnTick()
  {
    // ...

    if(success)
    {
      stats.trade(Threshold, Lot, Strategy);
    }
    else
    {
      OrdersCloseAll();
    }
  }

Теперь рассмотрим класс MyOLAPStats более подробно. Результаты OLAP-анализа обрабатывают методы display (основной виртуальный метод дисплея) и saveVector (вспомогательный).

  #define N_HOURS   24
  #define N_DAYS     5
  #define AXIS_HOURS 0
  #define AXIS_DAYS  1
  
  class MyOLAPStats: public Display
  {
    private:
      bool filled;
      double index[][3]; // value, hour, day
      int cursor;
  
    protected:
      bool saveVector(MetaCube *cube, const int &consts[], const SORT_BY sortby = SORT_BY_NONE)
      {
        PairArray *result = NULL;
        cube.getVector(0, consts, result, sortby);
        if(CheckPointer(result) == POINTER_DYNAMIC)
        {
          const int n = ArraySize(result.array);
          
          if(n == N_HOURS)
          {
            for(int i = 0; i < n; i++)
            {
              index[cursor][0] = result.array[i].value;
              index[cursor][1] = i;
              index[cursor][2] = consts[AXIS_DAYS];
              cursor++;
            }
          }
          
          delete result;
          return n == N_HOURS;
        }
        return false;
      }
  
    public:
      virtual void display(MetaCube *cube, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override
      {
        int consts[];
        const int n = cube.getDimension();
        ArrayResize(consts, n);
        ArrayInitialize(consts, 0);
  
        filled = false;
        
        ArrayResize(index, N_HOURS * N_DAYS);
        ArrayInitialize(index, 1);
        cursor = 0;
  
        if(n == 2)
        {
          const int i = AXIS_DAYS;
          int m = cube.getDimensionRange(i); // should be 5 work days
          for(int j = 0; j < m; j++)
          {
            consts[i] = j;
            
            if(!saveVector(cube, consts, sortby)) // 24 hours (values) per current day
            {
              Print("Bad data format");
              return;
            }
            
            consts[i] = 0;
          }
          filled = true;
          ArraySort(index);
          ArrayPrint(index);
        }
        else
        {
          Print("Incorrect cube structure");
        }
      }
      
      //...
  };

В классе описан двумерный массив index для хранения показателей эффективности торгов в привязке к расписанию. В методе display этот массив последовательно заполняется векторами из куба OLAP. Вспомогательная функция saveVector копирует числа для всех 24 часов за конкретный рабочий день. По второй размерности в массив index записываются последовательно значение показателя, номер часа и номер рабочего дня. Тот факт, что показатели находятся в первом (нулевом) элементе, позволяет сортировать массив по профит-фактору, но, в принципе, это нужно только для удобства отображения в логе.

На основании данных массива index осуществляется выбор торгового режима и непосредственно отправка приказов для тех отсчетов времени суток и дня недели, где превышен заданный порог ПФ.

    void trade(const double threshold, const double lots, const int strategy = 0)
    {
      const int h = TimeHour(lastBar);
      const int w = _TimeDayOfWeek() - 1;
    
      int mode = 0;
      
      for(int i = 0; i < N_HOURS * N_DAYS; i++)
      {
        if(index[i][1] == h && index[i][2] == w)
        {
          if(index[i][0] >= threshold)
          {
            mode = +1;
            Print("+ Rule ", i);
            break;
          }
          
          if(index[i][0] <= 1.0 / threshold)
          {
            mode = -1;
            Print("- Rule ", i);
            break;
          }
        }
      }
      
      // pick up existing orders (if any)
      const int direction = CurrentOrderDirection();
      
      if(mode == 0)
      {
        if(direction != 0)
        {
          OrdersCloseAll();
        }
        return;
      }
      
      if(strategy == 0)
      {
        if(direction != 0) // there exist open orders
        {
          if(mode == direction) // keep direction
          {
            return; // existing trade goes on
          }
          OrdersCloseAll();
        }
        
        const int type = mode > 0 ? OP_BUY : OP_SELL;
        
        const double p = type == OP_BUY ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
        const double sl = StopLoss > 0 ? (type == OP_BUY ? p - StopLoss * _Point : p + StopLoss * _Point) : 0;
          
        OrderSend(_Symbol, type, Lot, p, 100, sl, 0);
      }
      // ...
    }

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

Проведем оптимизацию эксперта OLAPQRWF на интервале от 2015 до 2019 год, а затем сделаем форвард-тест на 2019 году. Обратите внимание, что суть оптимизации фактически сводится к нахождению мета-параметров торговли — длительности OLAP-анализа, частоты перестроения OLAP-куба, выбора стратегии и кастом-поля агрегирования. В каждом прогоне во время оптимизации эксперт строит куб OLAP _на истории_ и торгует в своем виртуальном _будущем_, используя настройки из _прошлого_. Казалось бы, зачем в этом случае форвард-тест? Дело в том, что эффективность этой торговли напрямую зависит от указанных мета-параметров, и именно поэтому необходимо проверить применимость выбранных настроек на интервале out-of-sample.

Оптимизировать будем все параметры, влияющие на анализ, кроме периода Update (оставим его ежемесячным):

  • BarNumberLookBack=720||720||480||5760||Y
  • Threshold=2.0||2.0||0.5||5.0||Y
  • Strategy=0||0||1||1||Y
  • Update=0||0||0||1||N
  • CustomField=0||0||0||1||Y

Эксперт считает синтетический пользовательский показатель оптимизации, равный произведению коэффициента Шарпа и количества сделок. По этому показателю лучшим является прогноз со следующими входными параметрами эксперта:

  • BarNumberLookBack=2160
  • Threshold=3.0
  • Strategy=0
  • Update=monthly
  • CustomField=count

Запустим отдельный тест с 2015 по 2020 год и отметим поведение на форварде.

Рис.9 Отчет эксперта OLAPQRWF за период с 01.01.2015 до 01.01.2020 после "оптимизации" окна OLAP-анализа по 2018 год включительно, EURUSD H1

Рис.9 Отчет эксперта OLAPQRWF за период с 01.01.2015 до 01.01.2020 после "оптимизации" окна OLAP-анализа по 2018 год включительно, EURUSD H1

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

Заключение

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

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

Прикрепленные файлы |
MQLOLAP3.zip (66.21 KB)
Maxim Dmitrievsky
Maxim Dmitrievsky | 23 янв 2020 в 08:20
Развитие сезонной темы через олап, найс. Еще можно через встроенный лайт скл, наверное.
Stanislav Korotky
Stanislav Korotky | 23 янв 2020 в 12:19
Maxim Dmitrievsky:
Развитие сезонной темы через олап, найс. Еще можно через встроенный лайт скл, наверное.

Наверно, можно, но когда у меня начинался OLAP в 2016 году, SQL еще не было в МТ.

Stanislav Korotky
Stanislav Korotky | 28 янв 2020 в 20:26
Прикладываю исходные коды данной статьи, адаптированные по-быстрому под MT4. Должны нормально компилироваться, но полной проверки всего функционала не проводилось. Кое-какие вещи отсутствуют в MQL4 и не могут быть адекватно эмулированы, в частности, функция ArrayPrint с поддержкой многомерных массивов и массивов структур - для неё реализована простая заглушка без красивого вывода с выравниванием в строках логов. Желающие могут улучшить. Также здесь, как и в статье, не рассматривался и не портировался на MT4 графический интерфейс.
fxsaber
fxsaber | 19 фев 2020 в 09:17
Спасибо за статью! Правильно ли понимаю, что OLAP по смыслу теперь полностью пересекается с возможностями SQLite?
Stanislav Korotky
Stanislav Korotky | 19 фев 2020 в 10:24
fxsaber:
Спасибо за статью! Правильно ли понимаю, что OLAP по смыслу теперь полностью пересекается с возможностями SQLite?

Полностью не пересекается, скорее дополняют друг друга. Обычно OLAP - надстройка над базой и другими источниками данных. Писать запросы на SQL - это рутина. Задача OLAP - предоставить более человеческий интерфейс.

Работа с сетевыми функциями, или MySQL без DLL: Часть I - коннектор Работа с сетевыми функциями, или MySQL без DLL: Часть I - коннектор

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

Мультивалютный мониторинг торговых сигналов (Часть 1): Разработка структуры приложения Мультивалютный мониторинг торговых сигналов (Часть 1): Разработка структуры приложения

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

Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXXII): Отложенные торговые запросы - установка ордеров по условиям Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXXII): Отложенные торговые запросы - установка ордеров по условиям

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

Мультивалютный мониторинг торговых сигналов (Часть 2): Реализация визуальной части приложения Мультивалютный мониторинг торговых сигналов (Часть 2): Реализация визуальной части приложения

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