Прогнозирование временных рядов (Часть 1): метод эмпирической модовой декомпозиции (EMD)

Stanislav Korotky | 12 февраля, 2020

Введение

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

Тема прогнозирования — весьма обширна и затрагивалась на сайте mql5.com много раз. Одна из первых вводных и вместе с тем серьезных статей — Предсказание финансовых временных рядов — была опубликована в далеком 2008 году. Среди множества прочих статей и публикаций в CodeBase есть готовые к применению в MetaTrader инструменты, предлагающие, например:

Полный список можно получить в поиске по соответствующим разделам на сайте (статьи, CodeBase).

В контексте данного материала мы пополним перечень доступных инструментов прогнозирования двумя новыми. Первый из них основывается на методе эмпирической модовой декомпозиции (Empirical Mode Decomposition, EMD), который уже рассматривался в статье Знакомство с методом эмпирической модовой декомпозиции, но без применения к прогнозированию. EMD будет рассмотрен в этой первой части материала.

Второй инструмент использует метод опорных векторов (support-vector machine, SVM) в его модификации наименьших квадратов (Least-squares support-vector machine, LS-SVM). К нему мы обратимся во второй части.


Прогнозирующий алгоритм на основе EMD

Подробное введение в технологию EMD можно найти в статье Знакомство с методом эмпирической модовой декомпозиции. Суть её заключается в разложении временного ряда на простые составляющие — так называемые собственные характерные формы (Intrinsic Mode Functions, IMF). Каждая форма — это сплайн-интерполяция максимумов и минимумов временного ряда, причем сперва экстремумы ищутся для исходного ряда и из него вычитается только что найденная IMF, после чего сплайн-интерполяция производится уже для экстремумов модифицированного ряда, и данный процесс построения нескольких IMF продолжается до тех пор, пока остаток не станет меньше заданного уровня шума. Результат этой работы визуально напоминает разложение в ряд Фурье, но в отличие от последнего характерные формы EMD не являются гармоническими колебаниями с определенными частотами. Количество получаемых функций разложения IMF зависит от гладкости исходного ряда и настроек алгоритма.

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

К статье было приложено 2 файла: CEMDecomp.mqh и CEMD_2.mqh. Второй является слегка улучшенной версией первого, от него и будем отталкиваться. Скопируем его под новым именем EMD.mqh и пока без изменений включим в индикатор EMD.mq5.

  #include <EMD.mqh>

Также воспользуемся специальными классами для упрощенного объявления массива буферных индикаторов IndArray.mqh (описание на английском доступно в блоге, актуальная версия прилагается к статье). Буферов потребуется много и обрабатываться они будут единообразно.

  #define BUF_NUM 18 // 16 IMF maximum (including input at 0-th index) + residue + reconstruction
  
  #property indicator_separate_window
  #property indicator_buffers BUF_NUM
  #property indicator_plots   BUF_NUM
  
  #include <IndArray.mqh>
  IndicatorArray buffers(BUF_NUM);
  IndicatorArrayGetter getter(buffers);

Как видно, инидкатор выводится в отдельном окне, и в нем зарезервировано 18 буферов для отображения:

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

Но вернемся к файлу EMD.mqh. В нем определен класс CEMD, который и выполняет всю работу. Процесс запускается с помощью вызова метода decomp, куда передается массив с отсчетами временного ряда — y. Размер этого массива и определяет длину N фрагметов собственных функций — IMFResult. Вспомогательные массивы для их расчета подготавливает метод arrayprepare:

  class CEMD
  {
    private:
      int N;              // Input and output data size
      double IMFResult[]; // Result
      double X[];         // X-coordinate for the TimeSeries. X[]=0,1,2,...,N-1.
      ...
    
    public:
      int N;              // Input and output data size
      double Mean;        // Mean of input data
      ...
      
      int decomp(double &y[])
      {
        ...
        N = ArraySize(y);
        arrayprepare();
        for(i = 0; i < N; i++)
          X[i] = i;
        Mean = 0;
        for(i = 0; i < N; i++)
          Mean += (y[i] - Mean) / (i + 1.0); // Mean (average) of input data
        for(i = 0; i < N; i++)
        {
          a = y[i] - Mean;
          Imf[i] = a;
          IMFResult[i] = a;
        }
        // The loop of decomposition
          ...
          extrema(...);
          ...
        ...
      }
      
      
    private:
      int arrayprepare(void)
      {
        if(ArrayResize(IMFResult, N) != N) return (-1);
        ...
      }
  };

Для того чтобы увеличить количество расчетных точек, добавим в метод decomp новый параметр extrapolate, задающий глубину прогноза. Увеличим N на количество отсчетов, запрошенных в extrapolate, предварительно сохранив реальную длину исходного ряда в локальной переменной Nf (изменения помечены в коде комментариями со знаками "+" и "*" — они показывают, что "добавлено" и "изменено" соответственно).

      int decomp(const double &y[], const int extrapolate = 0) // *
      {
        ...
        N = ArraySize(y);
        int Nf = N;                            // + preserve actual number of input data points
        N += extrapolate;                      // + 
        arrayprepare();
        for(i = 0; i < N; i++)
          X[i] = i;
        Mean = 0;
        for(i = 0; i < Nf; i++)                // * was N
          Mean += (y[i] - Mean) / (i + 1.0);
        for(i = 0; i < N; i++)
        {
          a = y[MathMin(i, Nf - 1)] - Mean;    // * was y[i]
          Imf[i] = a;
          IMFResult[i] = a;
        }
        // The loop of decomposition
          ...
          extrema(...);
          ...
        for(i = 0; i < N; i++)
        {
          IMFResult[i + N * nIMF] = IMFResult[i];
          IMFResult[i] = y[MathMin(i, Nf - 1)] - Mean; // * was y[i]
        }
        
      }

Построение IMF на прогнозируемых барах начинается с последнего известного значения временного ряда.

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

Дело в том, что данный способ прогноза не совсем корректен. Поскольку мы увеличили размер N всех массивов объекта, это включает прогнозируемые бары в поиск экстремумов, который выполняется в методе extrema. Строго говоря, в будущем нет никаких экстремумов. Все экстремумы, которые там образуются в ходе вычислений, являются экстремумами суммы сплайн-экстраполяций (без исходного ряда, которого в будущем нет). В результате сплайн-функции начинают подстраиваться друг под друга, пытаясь сгладить свою суперпозицию. В некотором смысле это удобно, потому что прогноз получает самобалансировку — колебательный процесс остается вблизи значений временного ряда и не уходит в бесконечность. Однако ценность такого прогноза минимальна — он уже не характеризует исходный временной ряд. Вместе с тем данный способ, несомненно, имеет право на существование, и желающие могут его использовать, подключая в проект именно EMDloose.mqh.

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

Итак, нам требуется сделать так, чтобы функции IMF строились в будущем на сплайнах от последней реальной точки временного ряда. В этом случае глубина прогноза будет иметь физическое (прикладное) ограничение, поскольку кубические сплайны, без перестройки, стремятся к бесконечности. Это не критично, так как глубину прогноза изначально следует ограничить несколькими барами.

Суть изменений заключается в том, чтобы длину исходного временного ряда сохранять в переменной объекта, а не локально в методе decomp.

  class CEMD
  {
    private:
      int N;       // Input and output data size
      int Nf;      // +
      
    public:
        int decomp(const double &y[], const int extrapolate = 0)
        {
          ...
          N = ArraySize(y);
          Nf = N;                            // + preserve actual number of input data points in the object
          N += extrapolate;                  // +
          ...
        }
  };

Тогда мы можем использовать переменную Nf внутри метода extrema, заменив ею в соответствующих местах увеличенное N. Таким образом, в расчет будут браться только реальные экстремумы, происходящие из исходного ряда. Увидеть все правки проще всего с помощью контекстного сравнения файлов EMD.mqh и EMDloose.mqh.

На этом алгоритм прогнозирования фактически завершен. Осталось сделать маленький штрих, касающийся получения результатов декомпозиции. В классе CEMD для этой цели предназначен метод getIMF. Изначально в него передавались 2 параметра: приемный массив — x и номер запрашиваемой "гармоники" IMF — nn.

  void CEMD::getIMF(double &x[], const int nn, const bool reverse = false) const
  {
    ...
    if(reverse) ArrayReverse(x); // +
  }

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

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

Индикатор EMD.mq5

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

Определим входные параметры индикатора:

  input int Length = 300;  // Length (bars, > 5)
  input int Offset = 0;    // Offset (0..P bars)
  input int Forecast = 0;  // Forecast (0..N bars)
  input int Reconstruction = 0; // Reconstruction (0..M IMFs)

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

Параметр Forecast — это количество прогнозируемых баров. Для строгого алгоритма EMD.mqh рекомендуется не брать значение больше 5-10. Для упрощенного алгоритма EMDloose.mqh допустимы большие значения.

Параметр Reconstruction определяет количество функций IMF, которые нужно отбросить в процессе реконструкции временного ряда, так что остальные сформируют прогноз. Если здесь указан 0, реконструкция полностью совпадает с исходным рядом, и прогноз невозможен (строго говоря, он равен константе — последнему значению цены и потому не имеет смысла). Если указана 1, реконструкция будет сглаживаться за счет отбрасывания самых мелких колебаний, если — 2, отброшены будут две высших "гармоники" и так далее. Если ввести число, равное количеству найденных IMF функций, реконструкция совпадет с остатком ("трендом"). Во всех этих случаях, сглаженный ряд имеет прогноз (для каждого сочетания количества IMF — свой собственный). Если задано количество, большее числа IMF, реконструкция и прогноз не определены. Рекомендуемое значение для параметра равно 2.

Чем меньше величина Reconstruction, тем более подвижной и близкой к исходному ряду будет реконструкция (похоже на МА малого периода), но прогноз получится сильно волатильным. Чем эта величина больше, тем плавнее и стабильнее будет реконструкция и прогноз (похоже на МА старшего периода).

В обработчике OnInit зададим смещение буферов согласно глубине прогноза.

  int OnInit()
  {
    IndicatorSetString(INDICATOR_SHORTNAME, "EMD (" + (string)Length + ")");
    for(int i = 0; i < BUF_NUM; i++)
    {
      PlotIndexSetInteger(i, PLOT_DRAW_TYPE, DRAW_LINE);
      PlotIndexSetInteger(i, PLOT_SHIFT, Forecast);
    }
    return INIT_SUCCEEDED;
  }

Индикатор рассчитывается по ценам открытия в побаровом режиме. Вот основные моменты обработчика OnCalculate.

Описываем локальные переменные и устанавливаем индексацию используемых Open и Time как "таймсерий".

  int OnCalculate(const int rates_total,
                  const int prev_calculated,
                  const datetime& Time[],
                  const double& Open[],
                  const double& High[],
                  const double& Low[],
                  const double& Close[],
                  const long& Tick_volume[],
                  const long& Volume[],
                  const int& Spread[])
  {

    int i, ret;
    
    ArraySetAsSeries(Time, true);
    ArraySetAsSeries(Open, true);

Обеспечиваем побаровый режим.

    static datetime lastBar = 0;
    static int barCount = 0;
    
    if(Time[0] == lastBar && barCount == rates_total && prev_calculated != 0) return rates_total;
    lastBar = Time[0];
    barCount = rates_total;

Ждем достаточного количества данных.

    if(rates_total < Length || ArraySize(Time) < Length) return prev_calculated;
    if(rates_total - 1 < Offset || ArraySize(Time) - 1 < Offset) return prev_calculated;

Инициализируем индикаторные буфера.

    for(int k = 0; k < BUF_NUM; k++)
    {
      buffers[k].empty();
    }

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

    double yy[];
    int n = Length;
    ArrayResize(yy, n, n + Forecast);

Заполняем массив с временным рядом для анализа.

    for(i = 0; i < n; i++)
    {
      yy[i] = Open[n - i + Offset - 1]; // we need to reverse for extrapolation
    }

Запускаем EMD алгоритм с помощью соответствующего объекта.

    CEMD emd;
    ret = emd.decomp(yy, Forecast);
    
    if(ret < 0) return prev_calculated;

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

    const int N = emd.getN();
    const double mean = emd.getMean();

Расширяем массив yy, в который будем записывать точки каждой функции, на будущие бары.

    n += Forecast;
    ArrayResize(yy, n);

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

    for(i = 0; i < BUF_NUM; i++)
    {
      PlotIndexSetInteger(i, PLOT_SHOW_DATA, i <= N + 1);
      PlotIndexSetInteger(i, PLOT_LINE_WIDTH, i == N + 1 ? 2 : 1);
      PlotIndexSetInteger(i, PLOT_LINE_STYLE, STYLE_SOLID);
    }

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

    emd.getIMF(yy, 0, true);
    if(Forecast > 0)
    {
      for(i = 0; i < Forecast; i++) yy[i] = EMPTY_VALUE;
    }
    buffers[N + 1].set(Offset, yy);

Распределяем массив sum для реконструкции (суммы IMF). В цикле перебираем все IMF, которые участвуют в реконструкции, и суммируем отсчеты в этом массиве. Попутно выводим каждую IMF в свой буфер.

    double sum[];
    ArrayResize(sum, n);
    ArrayInitialize(sum, 0);
  
    for(i = 1; i < N; i++)
    {
      emd.getIMF(yy, i, true);
      buffers[i].set(Offset, yy);
      if(i > Reconstruction)
      {
        for(int j = 0; j < n; j++)
        {
          sum[j] += yy[j];
        }
      }
    }

Предпоследний буфер принимает остаток и отображается пунктиром.

    PlotIndexSetInteger(N, PLOT_LINE_STYLE, STYLE_DOT);
    emd.getIMF(yy, N, true);
    buffers[N].set(Offset, yy);

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

Наконец, завершаем суммирование компонентов по отсчетам в массиве sum, получая окончательную реконструкцию.

    for(int j = 0; j < n; j++)
    {
      sum[j] += yy[j];
      if(j < Forecast && (Reconstruction == 0 || Reconstruction > N - 1)) // completely fitted curve can not be forecasted (gives a constant)
      {
        sum[j] = EMPTY_VALUE;
      }
    }
    buffers[0].set(Offset, sum);
    
    return rates_total;
  }

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

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

Единственный нюанс, который стоит отметить, заключается в том, что при изменении смещения параметра Offset с помощью вертикальной линии индикатор запрашивает обновление чарта вызовом ChartSetSymbolPeriod. Эта функция реализована в MetaTrader 5 таким образом, что сбрасывает кэши всех таймфреймов текущего символа и перестраивает их заново. В зависимости от выбранной настройки по количеству баров на графиках и мощности компьютера этот процесс может занять заметное время (в некоторых случаях десятки секунд, если, например, есть графики M1 с миллионами баров). К сожалению, MQL API не предоставляет иного более экономного способа перестроения отдельного индикатора. В связи с этим, при возникновении указанной проблемы рекомендуется менять смещение через диалог свойств индикатора или уменьшить количество выводимых баров на графиках (требуется перезагрузка терминала). Вертикальная линия-курсор добавлена для удобного и точного интерактивного позиционирования в предполагаемое начало выборки данных.

Проверим как индикатор работает в строгом и упрощенном режимах при равных настройках (напомним, что упрощенный режим получается путем перекомпиляции с файлом EMDloose.mqh, т.к. не является основным рабочим). Для графика EURUSD D1 используем следующие настройки:

Короткий прогноз, индикаторы EMD, EURUSD D1

Короткий прогноз, индикаторы EMD, EURUSD D1

На скриншоте показано 2 версии индикатора — строгая сверху, упрощенная снизу. Обратите внимание, что в строгой версии некоторые "гармоники" стремятся "убежать" в разные стороны, вверх и вниз. За счет этого даже масштаб первого индикатора стал более мелким, чем второго (изменение масштаба является визуальным предупреждением о неадекватности глубины прогноза). В упрощенном режиме все компоненты разложения продолжают колебаться возле нуля. За счет этого можно получить более долгосрочный прогноз, подставив в параметр Forecast, например, значение 100. Это выглядит красиво, но, как правило, далеко от реальности. Единственным применением такого предсказания видится оценка будущего диапазона движений цен, который можно попытаться торговать на отскок внутрь и пробой наружу.

Длинный прогноз, индикаторы EMD, EURUSD D1

Длинный прогноз, индикаторы EMD, EURUSD D1

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

В заголовке индикаторов при увеличенном горизонте прогноза также видны различия: если изначально в обоих случаях было найден 6 собственных функций (второе число в скобках, после количества анализируемых баров), то теперь упрощенный вариант использует 7, поскольку в его случае запрошенные 100 баров прогноза участвуют в расчетах экстремумов. Прогноз на 10 баров не оказывает такого влияния (для данного временного ряда). Можно предположить, что Forecast = 10 — максимально допустимая, но не рекомендуемая длина прогноза. Рекомендуемая длина — 2-4 бара.

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

В EMDPrice используется короткая форма обработчика OnCalculate, что позволяет выбирать тип цены для расчета, например, типичную. Однако при любом типе кроме цены открытия следует иметь в виду, что индикатор расчитывается по открытию баров и потому последним сформированным (имеющим все типы цены) является бар 1. Иными словами, Offset может быть равен 0 только для цен открытия, а в остальных случаях — 1 и более.

На скриншоте ниже представлена работа индикатора EMDPrice со смещением в прошлое на 15 баров.

Прогноз индикатора EMDPrice на графике цены EURUSD D1

Прогноз индикатора EMDPrice на графике цены EURUSD D1, со смещением по истории

Для проверки прогностических способностей индикатора EMD разработаем специальный эксперт.

Тестовый эксперт на основе EMD

Создадим простой эксперт TestEMD, который будет создавать экземпляр индикатора EMD и на основе его прогноза совершать торговые операции. Работа будет вестись по открытию бара, так как индикатор использует для прогноза цены открытия.

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

В качестве торгового сигнала берется разница между показания индикатора на баре SignalBar (чтобы заглянуть в предсказываемое будущее, этот параметр предполагается отрицательным) и текущем нулевом баре. Положительная разница — сигнал на покупку, отрицательная — сигнал на продажу.

Поскольку индикатор EMD строит прогноз в будущем, номера баров в SignalBar обычно отрицательны и равны по модулю значению Forecast (в приципе, можно брать сигнал и с менее отдаленного бара, но тогда не понятно, зачем рассчитывать прогноз на большее число баров). Это касается штатного режима работы c выполнением торговых операций. В этом режиме, при вызове индикатора EMD, его параметр Offset всегда равен нулю, т.к. мы не исследуем прогнозы на истории.

Однако эксперт поддерживает и другой специальный неторговый режим, который позволяет ускоренно провести оптимизацию за счет теоретического расчета прибыльности виртуальных сделок на последних Forecast барах. Расчет выполняется последовательно на каждом новом баре выбранного диапазона дат, и общая статистика в виде профит-фактора произведения прогноза на реальный ход цены возвращается из OnTester. В тестере следует выбрать пользовательский критерий в качестве цели оптимизации. Для включения этого режима в параметр SignalBar следует ввести 0. При этом сам эксперт автоматически поставит Offset равным Forecast. Именно это позволяет эксперту сравнить прогноз и изменение цены на последних Forecast барах.

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

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

Полный исходный код эксперта приложен к статье и здесь подробно не описывается. Торговая часть основывается на библиотеке MT4Orders, которая упрощает вызов торговых функций. В эксперте отсутствует контроль ордеров по принципу "свой-чужой" с помощью магических номеров, строгая обработка ошибок, настройка проскальзываний, стоплоссов и тейкпрофитов. Фиксированный лот задается во входном параметре Lot, торговля ведется рыночными ордерами. Желающие применить EMD в рабочих экспертах могут дополнить данный тестовый эксперт соответствующими возможностями по необходимости или вставить работу с индикатором EMD по аналогии в свои существующие эксперты.

Пример настроек для оптимизации приложен к статье в виде файла TestEMD.set. Оптимизация по EURUSD D1 за 2018 год в ускоренном режиме дает следующий оптимальный "сет":

Соответственно, SignalBar должен быть равен Forecast со знаком минус, то есть -4.

Одиночный тест с этими настройками на периоде от начала 2018 вплоть до февраля 2020, то есть с форвардом по 2019 году и началу 2020-го, дает такую картину:

Отчет эксперта TestEMD для EURUSD D1, 2018-2020

Отчет эксперта TestEMD для EURUSD D1, 2018-2020

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

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

EMD — не единственная технология, которую мы рассмотрим в рамках данной статьи. Но прежде чем переходить ко второй части, нам потребуется "освежить" кое-какой математический аппарат для исследования временных рядов.

Анализ основных характеристик временных рядов в MQL — индикатор TSA

На сайте mql5.com уже публиковалась статья с похожим названием — Анализ основных характеристик временных рядов. В ней подробно рассмотрен расчет таких величин, как среднее, медиана, дисперсия, коэффициенты асимметрии и эксцесса, гистограмма распределения, функции автокорреляции, частной автокорреляции и многое другое. Все это сведено в класс TSAnalysis в файле TSAnalysis.mqh, который затем используется в демонстрационных целях в скрипте TSAexample.mq5. К сожалению, для визуализации работы класса был применен подход с генерацией внешнего HTML-файла, который нужно анализировать в браузере. Вместе с тем, MetaTrader 5 предоставляет различные графические средства для отображения массивов данных, в первую очередь индикаторные буфера. Мы слегка модифицируем класс и сделаем его более "дружественным" для индикаторов, после чего реализуем индикатор, позволяющий анализировать котировки непосредственно в терминале.

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

Скалярные характерстики сведем в единую структуру TSStatMeasures:

  struct TSStatMeasures
  {
    double MinTS;      // Minimum time series value
    double MaxTS;      // Maximum time series value
    double Median;     // Median
    double Mean;       // Mean (average)
    double Var;        // Variance
    double uVar;       // Unbiased variance
    double StDev;      // Standard deviation
    double uStDev;     // Unbiaced standard deviation
    double Skew;       // Skewness
    double Kurt;       // Kurtosis
    double ExKurt;     // Excess Kurtosis
    double JBTest;     // Jarque-Bera test
    double JBpVal;     // JB test p-value
    double AJBTest;    // Adjusted Jarque-Bera test
    double AJBpVal;    // AJB test p-values
    double maxOut;     // Sequence Plot. Border of outliers
    double minOut;     // Sequence Plot. Border of outliers
    double UPLim;      // ACF. Upper limit (5% significance level)
    double LOLim;      // ACF. Lower limit (5% significance level)
    int NLags;         // Number of lags for ACF and PACF Plot
    int IP;            // Autoregressive model order
  };

Массивы обозначим пунктами перечисления TSA_TYPE:

  enum TSA_TYPE
  {
    tsa_TimeSeries,
    tsa_TimeSeriesSorted,
    tsa_TimeSeriesCentered,
    tsa_HistogramX,
    tsa_HistogramY,
    tsa_NormalProbabilityX,
    tsa_ACF,
    tsa_ACFConfidenceBandUpper,
    tsa_ACFConfidenceBandLower,
    tsa_ACFSpectrumY,
    tsa_PACF,
    tsa_ARSpectrumY,
    tsa_Size //  
  };        //  ^ non-breaking space (to hide aux element tsa_Size name)

Для получения заполненной структуры TSStatMeasures с результатами работы предусмотрен метод getStatMeasures. Для получения любого из массивов с помощью макросов сгенерированы однотипные методы вида getARRAYNAME, где ARRAYNAME соответствует суффиксу одного из элементов перечисления TSA_TYPE. Например, чтобы прочитать отсортированную таймсерию, нужно вызвать метод getTimeSeriesSorted. Все такие методы имеют сигнатуру:

  int getARRAYNAME(double &result[]) const;

заполняют переданный массив и возвращают количество элементов.

Кроме того, имеется универсальный метод для чтения любого массива:

  int getResult(const TSA_TYPE type, double &result[]) const

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

Обработку котировок с помощью класса TSAnalysis удобно проводить из специального индикатора — TSA.mq5. Его основная цель — визуализация характеристик, представляющих собой массивы. Желающие могут при необходимости дополнить его возможностью выводить скалярные величины (сейчас они печатаются в лог).

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

Входные параметры индикатора:

Индикатор рассчитывается по барам.

Вот, например, как выглядит частная автокорреляционная функция для EURUSD D1 на 500 барах, с дифференцированием:

Индикатор TSD, EURUSD D1

Индикатор TSD, EURUSD D1

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

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


Заключение

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