English
preview
Цифровая обработка сигналов для трейдеров: Создаем библиотеку фильтров Элерса на языке MQL5

Цифровая обработка сигналов для трейдеров: Создаем библиотеку фильтров Элерса на языке MQL5

MetaTrader 5 — Торговые системы |
57 0
Adeolu Kayode Gbadebo
Adeolu Kayode Gbadebo

Содержание

  1. Введение
  2. Почему цены нужно фильтровать, а не просто усреднять
  3. Разработка библиотеки: единый источник данных и расчётов
  4. Super Smoother: фильтр нижних частот (ФНЧ) с малым запаздыванием
  5. Roofing Filter: сохранение только диапазона периодов, пригодного для торговли
  6. Even Better Sinewave: циклический осциллятор, который понимает, когда ошибается
  7. Заключение


Введение

Еще до того, как он написал хотя бы одну строку кода для торговли, Джон Ф. Элерс много лет проработал инженером, разрабатывая фильтры для радиолокационных систем и систем связи. Когда он обратился к финансовым рынкам, он применил этот набор инструментов и выдвинул тезис, над которым большинство технических аналитиков даже не задумываются: ценовой график — это сигнал именно в том смысле, в каком этот термин понимает инженер-электротехник. Он несет информацию, смешанную с шумом, содержит компоненты разных частот и может обрабатываться с помощью тех же математических методов, которые используются для очистки радиосигнала от шума. Большинство индикаторов, которыми трейдеры пользуются ежедневно, — особенно простая скользящая средняя — представляют собой грубые фильтры. Инженер не стал бы их использовать, поскольку они сильно запаздывают и пропускают именно тот шум, который должны подавлять.

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

Эта тема уже частично освещена на MQL5, и стоит уточнить, что уже существует, чтобы понять, что нового добавляет эта серия статей. Статья "Теория и реализация расширенных адаптивных индикаторов в MQL5" была опубликована ещё в 2011 году и остаётся единственной на сайте, в которой непосредственно рассматриваются методы Элерса. Это хорошая статья, но в ней реализованы три индикатора из его первой книги "Кибернетический анализ акций и фьючерсов": "Адаптивный киберцикл", "Адаптивный центр тяжести" и "Адаптивный RVI". Эта статья появилась раньше большинства инструментов, которыми Элерс наиболее известен сегодня. В двух более старых статьях — "Технические индикаторы и цифровые фильтры" и "Создание цифровых фильтров без запаздывания" — индикаторы рассматриваются в общем плане как цифровые фильтры, однако в них излагается теория, а не реализуются конкретные авторские фильтры Элерса. Не хватает точной, современной реализации опубликованных фильтров Элерса, пригодной для повторного использования ("Аналитика циклов для трейдеров", 2013; статьи по MESA): Super Smoother, Roofing Filter, измерения доминирующего цикла, адаптивной скользящей средней MESA (MAMA) и Even Better Sinewave. Именно эту лакуну и заполняет данная серия.

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

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


Почему цены нужно фильтровать, а не просто усреднять

Начнём с той проблемы, которую пытается решить каждый индикатор сглаживания. График необработанной цены имеет изломанный вид. Внутри любого движения присутствует множество мелких и быстрых колебаний, которые практически ничего не говорят о направлении, а под ними скрывается более медленная структура — именно на ней и стоит строить торговлю. Задача сглаживателя заключается в подавлении быстрых колебаний при сохранении медленной структуры. На языке обработки сигналов быстрые колебания представляют собой высокочастотную составляющую, а медленная структура — низкочастотную, поэтому сглаживатель является фильтром нижних частот (ФНЧ): он пропускает низкие частоты и ослабляет высокие.

Простая скользящая средняя представляет собой фильтр нижних частот (ФНЧ), но весьма неэффективный, и стоит подробнее объяснить почему. У неё сразу два недостатка. Во-первых, она запаздывает: поскольку она усредняет значения за последние N баров, её выходное значение отстаёт от цены на половину длины окна, поэтому SMA с периодом 20 баров отражает состояние рынка примерно десять баров назад. Во-вторых, и этому уделяют меньше внимания, она плохо справляется с подавлением шума. Её частотная характеристика имеет выраженную неравномерность, поэтому некоторые высокочастотные составляющие подавляются недостаточно и могут даже менять фазу на противоположную, что проявляется в колебаниях SMA там, где трейдер ожидал плавную линию. Вы расплачиваетесь запаздыванием, но взамен даже не получаете чистого сигнала.

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

Вторая идея не менее важна и объясняет, почему одного фильтра нижних частот (ФНЧ) недостаточно. В цене присутствуют не только шум, от которого мы хотим избавиться, и тренд, который мы хотим сохранить. Кроме того, она содержит медленно смещающуюся базовую линию — долгосрочную тенденцию, которая во многих случаях сама по себе является своего рода помехой. Если вы пытаетесь измерить рыночный цикл — ритмические колебания вокруг справедливой стоимости, — то сильный тренд, на который накладывается этот цикл, искажает результаты измерения; этот эффект Элерс называет расширением спектра. Для этого нужно также удалить самые низкие частоты с помощью фильтра верхних частот (ФВЧ), оставив лишь промежуточный диапазон периодов циклов. Именно так устроен Roofing Filter: фильтр нижних частот (ФНЧ) и фильтр верхних частот (ФВЧ) работают совместно, выделяя определённую полосу частот; мы реализуем его в разделе 5.

Разложение исходной цены на шумовую, циклическую и трендовую составляющие

Рис. 1. Ценовой ряд представляет собой сумму составляющих с разными временными масштабами: быстрого шума, который мы хотим устранить, цикла в промежуточном диапазоне, пригодного для торговли, и медленно меняющейся трендовой базовой линии. Фильтры позволяют сохранить нужную нам полосу частот.

Запомните простую модель: фильтр — это инструмент, позволяющий пропускать одни частоты и подавлять другие. Фильтр нижних частот (ФНЧ) пропускает медленные составляющие сигнала. Фильтр верхних частот (ФВЧ) пропускает быстрые составляющие сигнала. При каскадном соединении фильтра верхних частот (ФВЧ) с фильтром нижних частот (ФНЧ) сохраняется полоса частот между их частотами среза. Каждый класс в библиотеке, которую мы собираемся создать, относится к одному из этих трёх типов.


Проектирование библиотеки: единый источник данных и расчётов

Прежде чем приступать к реализации фильтров, примем архитектурное решение. Эти подпрограммы цифровой обработки сигналов (ЦОС) будут использоваться в программах двух типов: в визуальных индикаторах, которые рисуют линию в подокне, и, во второй части статьи, в советнике, принимающем торговые решения на основе тех же числовых значений. Неправильный подход — реализовать одни и те же расчёты дважды: отдельно для индикатора и отдельно для советника. Эти реализации со временем расходились бы, а ошибка, исправленная в одной, оставалась бы в другой. Правильный подход — реализовать каждый фильтр ровно один раз в виде класса и поместить все классы в один подключаемый файл, используемый и индикаторами, и советником. Тогда советник вычисляет значения фильтра непосредственно в собственном процессе, а не считывает их через iCustom, что ускоряет работу и устраняет целую категорию ошибок, связанных с управлением дескрипторами.

Таким образом, вся библиотека находится в одном заголовочном файле EhlersDSP.mqh, в подпапке Ehlers папки Include. Ниже приведён заголовок файла, в котором также изложен наш принцип работы с первоисточниками: формулы и коэффициенты перенесены без изменений из опубликованных листингов Элерса, за исключением одной неизбежной поправки — в EasyLanguage используются градусы, тогда как тригонометрические функции MQL5 работают с радианами.

//+------------------------------------------------------------------+
//|                                                    EhlersDSP.mqh |
//|   Digital Signal Processing toolkit after John F. Ehlers         |
//|                                                                  |
//|   One source of truth for the DSP math used by both the visual   |
//|   indicators and the regime-switching EA. Each class is a        |
//|   stateful recursive (IIR) filter: feed it one new value per bar |
//|   with Update(); read the latest output with Value().            |
//|                                                                  |
//|   Formulas are ported verbatim from Ehlers' published listings   |
//|   (TASC Traders' Tips, "Cycle Analytics for Traders" 2013, and   |
//|   the MESA "MAMA" paper 2001). EasyLanguage trig is in DEGREES;  |
//|   here we convert to radians. Coefficients are preserved exactly |
//+------------------------------------------------------------------+
#property copyright "Adeolu Kayode"
#property strict

#define EHLERS_PI 3.14159265358979323846

Термин рекурсивный фильтр с бесконечной импульсной характеристикой (БИХ-фильтр), сохраняющий внутреннее состояние, использованный в этом заголовке, — ключ ко всей архитектуре, поэтому стоит разобрать его подробнее. IIR расшифровывается как infinite impulse response — "бесконечная импульсная характеристика" (БИХ); так инженеры называют фильтр, выход которого зависит не только от входного сигнала, но и от предыдущих значений собственного выхода. Каждый фильтр в этой библиотеке рекурсивен именно в этом смысле: для вычисления текущего значения ему нужны одно или два значения, полученные им на предыдущих барах. Поэтому каждый фильтр должен сохранять внутреннее состояние: ему необходимо помнить свою недавнюю историю, и это определяет структуру классов. Каждый фильтр представляет собой объект, который создаётся один раз, а затем получает по одному новому значению цены через метод Update(), сохраняя своё внутреннее состояние между вызовами.

Кольцевой буфер с индексацией как в книге. Элерс записывает свои формулы на языке EasyLanguage, где x[n] обозначает значение x на n баров назад. Чтобы перенесённый код можно было построчно сопоставлять с оригиналами Элерса, мы храним для каждого сигнала небольшую историю фиксированного размера с той же схемой индексации: индекс 0 соответствует текущему значению, а индекс 1 — значению на один бар назад. В этом и заключается вся задача вспомогательного класса SignalHistory.

//+------------------------------------------------------------------+
//  Small ring buffer for the last N samples of a signal.            |
//  history[0] is the most recent value, history[1] one bar ago, …   |
//  This mirrors EasyLanguage's `x[n]` (n bars ago) notation so the  |
//  ported formulas read identically to Ehlers' originals.           |
//+------------------------------------------------------------------+
class SignalHistory
  {
private:
   double            m_buf[];   // ring storage, m_buf[0] = most recent
   int               m_size;    // fixed capacity (oldest sample dropped past this)
   int               m_count;   // how many real samples seen so far
public:
   void              Init(const int size)
     {
      m_size = (size < 1 ? 1 : size);
      ArrayResize(m_buf, m_size);
      ArrayInitialize(m_buf, 0.0);
      m_count = 0;
     }
   //--- push a new most-recent value, shifting older ones back
   void              Push(const double v)
     {
      for(int i = m_size - 1; i > 0; i--)
         m_buf[i] = m_buf[i - 1];
      m_buf[0] = v;
      if(m_count < m_size)
         m_count++;
     }
   //--- value n bars ago (0 = current). Reads as Ehlers' x[n].
   double            operator[](const int n) const
     {
      if(n < 0 || n >= m_size)
         return(0.0);
      return(m_buf[n]);
     }
   int               Count() const { return(m_count); }
   bool              Ready(const int need) const { return(m_count >= need); }
  };

Две детали заслуживают особого внимания. Именно перегруженный оператор индексации позволяет в дальнейшем коде использовать m_hp[1] для обозначения "выходного значения фильтра верхних частот (ФВЧ) на один бар назад", так же как в исходном коде на EasyLanguage. А метод Ready() нужен потому, что расчёт рекурсивного фильтра не имеет смысла, пока не накопится достаточно истории для подстановки в формулу; до этого каждый класс использует подходящее значение для прогрева, а не трактует нули как реальные отсчёты сигнала. Обращайте внимание на оба этих момента по ходу работы.


Super Smoother: фильтр нижних частот (ФНЧ) с малым запаздыванием

Super Smoother — это основа всей библиотеки. Это предложенная Элерсом замена скользящей средней: двухполюсный фильтр нижних частот (ФНЧ), созданный по образцу аналоговых фильтров, используемых в электронике, который подавляет высокочастотный шум гораздо эффективнее, чем SMA или EMA с сопоставимым запаздыванием. Почти все остальные инструменты, которые мы создаём, используют этот фильтр как компонент, поэтому мы реализуем его первым, а затем используем повсюду.

У фильтра три коэффициента, которые вычисляются однократно на основе единственного параметра — периода среза. Циклы с периодами значительно короче периода среза сильно ослабляются, а циклы с периодами значительно длиннее проходят через фильтр. Формулы Элерса для коэффициентов при принятом им по умолчанию периоде среза 10 имеют следующий вид:

a1 = exp(-1.414 * pi / Period)

b1 = 2 * a1 * cos(1.414 * 180 / Period), где угол выражен в градусах

c2 = b1, c3 = -a1 * a1, c1 = 1 - c2 - c3

Множитель 1.414 — приближённое значение квадратного корня из 2; он задаёт положение двух полюсов фильтра, обеспечивающее плавный отклик с хорошим затуханием (это условие Баттерворта). Условие c1 = 1 - c2 - c3 тоже не случайно: оно гарантирует, что сумма коэффициентов равна единице, поэтому постоянный входной сигнал проходит без усиления или ослабления. После вычисления коэффициентов каждое новое выходное значение формируется из текущего и предыдущего входных значений, а также обратной связи по двум предыдущим выходным значениям:

Filt = c1 * (Price + Price[1]) / 2 + c2 * Filt[1] + c3 * Filt[2]

Слагаемые c2 * Filt[1] + c3 * Filt[2] задают рекурсию: фильтр использует собственные предыдущие значения, поэтому он является фильтром с бесконечной импульсной характеристикой (БИХ-фильтром), а класс должен хранить внутреннее состояние. Практический результат виден на рис. 2: при сопоставимом запаздывании фильтр Super Smoother дает заметно более чистую линию, чем простая скользящая средняя, поскольку его частотная характеристика обеспечивает плавное подавление шума, а не пропускает пульсации.

Super Smoother и простая скользящая средняя на одних и тех же данных с шумом

Рис. 2. Один и тот же шумный ряд, сглаженный двумя способами. Простая скользящая средняя запаздывает и по-прежнему дает пульсации; фильтр Super Smoother точнее следует за исходным рядом, формируя более чистую линию без шума.

Вот реализация. Обратите внимание, где выполняется преобразование градусов в радианы: комментарий в строке с b1 поясняет, что угол 1.414 * 180 / Period градусов равен углу 1.414 * pi / Period радиан, который мы и передаем в функцию MathCos.

//+------------------------------------------------------------------+
//  SuperSmoother — Ehlers' 2-pole Butterworth low-pass filter.      |
//  His low-lag replacement for SMA/EMA. Critical period default 10. |
//  Ref: TASC Jan-2014 Traders' Tips; Cycle Analytics ch.3.          |
//+------------------------------------------------------------------+
class SuperSmoother
  {
private:
   double            m_c1, m_c2, m_c3; // recursion coefficients (set in Init)
   SignalHistory     m_in;      // raw input history (need 2 back)
   SignalHistory     m_out;     // filtered output history (need 2 back)
   int               m_count;   // samples processed so far (for warm-up)
public:
   void              Init(const double period = 10.0)
     {
      double a1 = MathExp(-1.414 * EHLERS_PI / period);
      double b1 = 2.0 * a1 * MathCos(1.414 * EHLERS_PI / period); // 1.414*180/period deg = 1.414*PI/period rad
      m_c2 = b1;
      m_c3 = -a1 * a1;
      m_c1 = 1.0 - m_c2 - m_c3;
      m_in.Init(2);
      m_out.Init(2);
      m_count = 0;
     }
   //--- feed one new raw sample, return the new filtered value
   double            Update(const double x)
     {
      double filt;
      if(m_count < 2)            // warm-up: pass input through
         filt = x;
      else
         filt = m_c1 * (x + m_in[0]) / 2.0 + m_c2 * m_out[0] + m_c3 * m_out[1];
      m_in.Push(x);
      m_out.Push(filt);
      m_count++;
      return(filt);
     }
   double            Value() const { return(m_out[0]); }
  };

О ветке прогрева стоит сказать отдельно: в исходных листингах Элерса этот случай не рассматривается, и при небрежном переносе кода здесь легко допустить ошибку. На первых двух барах для рекурсивного расчета еще нет фактических значений Filt[1] и Filt[2], на которые можно опереться; если считать их нулевыми, возникнет сильный искусственный переходный процесс, который из-за рекурсивной природы фильтра будет затухать много баров. Если же на первых двух барах передавать входное значение напрямую на выход, рекурсивный расчет получит корректные начальные значения. Эта небольшая деталь заметно улучшает результат фильтрации на первых двенадцати барах каждого графика.


Roofing Filter: сохранение только диапазона периодов, пригодного для торговли

Фильтр Super Smoother удаляет шум, лежащий выше интересующей нас полосы частот. Roofing Filter дополняет эту обработку: он также устраняет медленный тренд ниже полосы пропускания, оставляя чистые колебания с почти нулевым средним в диапазоне периодов, пригодном для торговли. Элерс назвал его Roofing Filter, поскольку ступень фильтрации верхних частот устанавливает своего рода "крышу" для анализа, ограничивая максимальный период пропускаемого цикла. По умолчанию диапазон периодов простирается примерно от 10 баров (граница задается фильтром Super Smoother) до 48 баров (граница задается фильтром верхних частот (ФВЧ)).

Новый элемент — двухполюсный фильтр верхних частот (ФВЧ), который применяется первым. Его единственный коэффициент рассчитывается по периоду фильтра верхних частот (ФВЧ), по умолчанию равному 48 барам, и константе 0.707 (снова единица, деленная на квадратный корень из двух), которая задает затухание:

alpha1 = (cos(0.707 * 360 / 48) + sin(0.707 * 360 / 48) - 1) / cos(0.707 * 360 / 48), угол в градусах

Затем выходной сигнал фильтра верхних частот (ФВЧ) вычисляется рекурсивно на основе второй разности цены, которая и подавляет тренд, с добавлением обратной связи по двум предыдущим значениям выходного сигнала:

HP = (1 - alpha1/2)^2 * (Price - 2*Price[1] + Price[2]) + 2*(1 - alpha1)*HP[1] - (1 - alpha1)^2 * HP[2]

Выходной сигнал фильтра верхних частот (ФВЧ), уже очищенный от тренда, но всё ещё содержащий шум, подается непосредственно на фильтр Super Smoother для удаления шума. Поскольку мы уже реализовали SuperSmoother в виде класса, Roofing Filter просто содержит его экземпляр и делегирует ему обработку. В этом и заключается преимущество архитектуры библиотеки: каскадное соединение описывается в коде так же кратко, как и в математических формулах. На рис. 3 показан путь прохождения сигнала.

Путь прохождения сигнала в Roofing Filter: цена → фильтр верхних частот (ФВЧ) → Super Smoother

Рис. 3. Roofing Filter как каскадное соединение фильтров. Цена поступает на двухполюсный фильтр верхних частот (ФВЧ), который удаляет медленную трендовую составляющую, затем сигнал проходит через Super Smoother, который удаляет высокочастотный шум, оставляя диапазон периодов, пригодный для торговли.

//+------------------------------------------------------------------+
//  RoofingFilter — 2-pole high-pass (removes periods > 48 bars)     |
//  cascaded into a SuperSmoother (removes periods < ~10 bars).      |
//  Passes only the tradeable band; output has ~zero mean.           |
//  Ref: Cycle Analytics ch.7; EasyLanguageMastery code listing.     |
//+------------------------------------------------------------------+
class RoofingFilter
  {
private:
   double            m_alpha1;  // high-pass coefficient (set in Init)
   SignalHistory     m_price;   // raw price history (need 2 back)
   SignalHistory     m_hp;      // high-pass output history (need 2 back)
   SuperSmoother     m_ss;      // low-pass stage applied to the HP output
   int               m_count;   // samples processed so far (for warm-up)
public:
   void              Init(const double hpPeriod = 48.0, const double ssPeriod = 10.0)
     {
      //--- 2-pole high-pass coefficient (.707 = critical damping)
      double arg = 0.707 * 2.0 * EHLERS_PI / hpPeriod;   // .707*360/hp deg
      m_alpha1 = (MathCos(arg) + MathSin(arg) - 1.0) / MathCos(arg);
      m_price.Init(3);
      m_hp.Init(2);
      m_ss.Init(ssPeriod);
      m_count = 0;
     }
   //--- feed raw price; returns the roofing-filtered value
   double            Update(const double price)
     {
      double hp;
      if(m_count < 2)
         hp = 0.0;               // warm-up
      else
        {
         double one = 1.0 - m_alpha1;
         hp = (1.0 - m_alpha1 / 2.0) * (1.0 - m_alpha1 / 2.0) *
              (price - 2.0 * m_price[0] + m_price[1])
              + 2.0 * one * m_hp[0] - one * one * m_hp[1];
        }
      m_price.Push(price);
      m_hp.Push(hp);
      m_count++;
      return(m_ss.Update(hp));   // smooth the HP output
     }
   double            Value() const { return(m_ss.Value()); }
  };

Здесь стоит обратить внимание на одну тонкость, ведь именно такие моменты читатель, копирующий код, должен понимать, а не просто принимать на веру. В формуле Price[1] и Price[2] обозначают значения цены один и два бара назад соответственно. В коде на момент вычисления hp текущая цена ещё не добавлена в буфер, поэтому он по-прежнему содержит цены предыдущих баров: m_price[0] — цену один бар назад, а m_price[1] — цену два бара назад. Текущая цена передается в аргументе price. Соответствие индексов верно, но сдвиг обусловлен тем, в какой момент цена добавляется в буфер; при невнимательном чтении это можно принять за ошибку смещения индекса на единицу, хотя ошибки здесь нет.

Превращение фильтра в индикатор. Основная сложность — в реализации библиотеки; индикатор на её основе требует совсем немного кода. Индикатор Roofing Filter объявляет один буфер, создает один экземпляр класса RoofingFilter и заполняет буфер бар за баром. Единственное проектное решение, заслуживающее обсуждения, относится к функции OnCalculate.

//+------------------------------------------------------------------+
//|                                       EhlersRoofingFilter.mq5    |
//|   Roofing Filter (2-pole HP + Super Smoother) after J. F. Ehlers |
//|   Passes only the tradeable band of cycle periods (~10..48 bars).|
//+------------------------------------------------------------------+
#property copyright "Adeolu Kayode"
#property strict
#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1

#property indicator_label1  "Roofing"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrDodgerBlue
#property indicator_width1  2
#property indicator_level1  0.0

#include <Ehlers/EhlersDSP.mqh>

input double InpHPPeriod = 48.0;   // High-pass period (longest passed cycle)
input double InpSSPeriod = 10.0;   // Super Smoother period (shortest passed cycle)

double         RoofBuffer[];       // plotted indicator buffer (roofing-filtered series)
RoofingFilter  g_roof;             // stateful roofing filter (HP + Super Smoother)

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   SetIndexBuffer(0, RoofBuffer, INDICATOR_DATA);
   IndicatorSetString(INDICATOR_SHORTNAME,
                      StringFormat("Roofing(%.0f,%.0f)", InpHPPeriod, InpSSPeriod));
   IndicatorSetInteger(INDICATOR_DIGITS, 5);
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Stateful IIR filters must be replayed from the start, so we      |
//| rebuild the whole series whenever a fresh calc is requested.     |
//+------------------------------------------------------------------+
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[])
  {
   if(rates_total < 8)
      return(0);

//--- Recursive IIR state must stay consistent across the whole series.
//--- Re-initialise and replay every call: simplest correct approach, and
//--- cheap for the few thousand bars a chart holds.
   g_roof.Init(InpHPPeriod, InpSSPeriod);
   for(int i = 0; i < rates_total; i++)
     {
      double price = (high[i] + low[i]) / 2.0;
      RoofBuffer[i] = g_roof.Update(price);
     }
   return(rates_total);
  }

В большинстве индикаторов работа функции OnCalculate оптимизируется за счет обработки баров начиная с позиции, определяемой значением prev_calculated, так что пересчитываются лишь самые последние значения. Мы сознательно не делаем этого в данном случае, и причина заключается в рекурсивном характере фильтра. Класс хранит внутреннее состояние — предыдущие выходные значения, используемые в рекурсивном расчете, — и это состояние имеет смысл только в том случае, если фильтр последовательно обработал каждый бар с самого начала. Нельзя корректно возобновить работу рекурсивного фильтра с бесконечной импульсной характеристикой (БИХ-фильтра), сохраняющего внутреннее состояние, с середины ряда, если его точное внутреннее состояние в этой точке не было сохранено. Корректное и надежное решение — при каждом вызове заново инициализировать фильтр и повторно обрабатывать весь ряд. Для нескольких тысяч баров на графике такая обработка выполняется практически мгновенно, а сам подход исключает трудноуловимые ошибки. На вход фильтра мы подаем медианную цену, (максимум + минимум) / 2, которую Элерс повсеместно использует как несколько более "чистый" входной сигнал по сравнению с ценой закрытия.

На графике Roofing Filter отображается в отдельном подокне в виде линии, колеблющейся вокруг нуля. Поскольку тренд удален фильтром верхних частот (ФВЧ), а шум — фильтром Super Smoother, остаются колебания рынка с периодами от 10 до 48 баров, центрированные относительно нуля; по ним гораздо легче анализировать циклическую структуру, чем по исходной цене.

Roofing Filter в подокне под ценовым графиком

Рис. 4. Roofing Filter (нижнее подокно), примененный к цене. Тренд и шум удалены; остаются колебания с почти нулевым средним значением в диапазоне периодов, пригодном для торговли.


Even Better Sinewave: циклический осциллятор, который знает, когда ошибается

Последний инструмент в этой части — самый интересный: он закладывает основу всей стратегии из части 2. "Even Better Sinewave" — это циклический осциллятор, но его истинная ценность заключается в том, как он ведет себя в условиях отсутствия цикличности на рынке. Большинство осцилляторов, отслеживающих колебания цены, при тренде дают катастрофически неверные сигналы: они достигают предельного уровня, кричат о "перекупленности" и остаются у этого уровня, пока тренд продолжается, а трейдер, открывший позицию против него, несет убытки. В алгоритме Элерса этот недостаток превращается в сигнал. Когда на рынке развивается тренд, Even Better Sinewave по замыслу удерживается у предельного уровня, вблизи +1 или −1, и именно это удержание позволяет определить, что рынок перешел из циклического режима в трендовый.

Это достигается с помощью приёма нормализации. Сначала цену пропускают через фильтр верхних частот (ФВЧ) и Super Smoother — по тому же принципу, что и в Roofing Filter, однако здесь Элерс использует более простой однополюсный ФВЧ с более длинным периодом среза, чтобы намеренно сохранить большую часть тренда. Затем по последним трем барам вычисляются две величины: среднее значение волны за три бара и средняя мощность (среднее арифметическое квадратов значений). Выходное значение равно амплитуде, деленной на квадратный корень из мощности:

Wave = (Filt + Filt[1] + Filt[2]) / 3

Pwr = (Filt^2 + Filt[1]^2 + Filt[2]^2) / 3

Output = Wave / sqrt(Pwr)

Стоит посмотреть, почему это работает. Когда рынок действительно находится в циклическом режиме, отфильтрованная волна симметрично колеблется относительно нуля, средняя амплитуда за три бара остается значительно ниже квадратного корня из мощности, а их отношение плавно колеблется примерно от -1 до +1. Когда на рынке наблюдается тренд, отфильтрованные значения перестают менять знак и становятся одинаковыми по модулю, поэтому амплитуда и квадратный корень из мощности оказываются почти равными, а их отношение достигает предельного уровня +1 или -1. Коэффициент однополюсного фильтра верхних частот (ФВЧ), с которого начинается обработка, рассчитывается по параметру Duration, равному по умолчанию 40:

alpha1 = (1 - sin(360 / Duration)) / cos(360 / Duration), угол в градусах

Вот этот класс. Класс построен по уже знакомой схеме: коэффициенты вычисляются один раз в Init(), затем в Update() выполняется фильтрация однополюсным фильтром верхних частот (ФВЧ), его выход обрабатывается по формулам Super Smoother прямо в коде метода, а после получения трех отфильтрованных отсчетов применяется нормализация волны по квадратному корню из мощности.

//+------------------------------------------------------------------+
//  EvenBetterSinewave — normalized cycle oscillator that also flags |
//  trend mode. Single-pole HP (retains longer trend) -> Super       |
//  Smoother -> 3-bar wave power normalization (Wave/sqrt(Pwr)).     |
//  Output ~ -1..+1 in cycle mode; rails near +/-1 in trend mode.    |
//  Ref: Cycle Analytics ch.12, code listing 12-1. Duration def 40.  |
//+------------------------------------------------------------------+
class EvenBetterSinewave
  {
private:
   double            m_alpha1;    // single-pole high-pass coefficient (set in Init)
   double            m_c1, m_c2, m_c3; // super smoother coefficients (set in Init)
   SignalHistory     m_price;     // raw price history (need 1 back; HP uses close-close[1])
   SignalHistory     m_hp;        // high-pass output history (need 1 back)
   SignalHistory     m_filt;      // smoothed output history (need 2 back: smoother + wave)
   double            m_wave;      // normalized sinewave value (the public output)
   int               m_count;     // samples processed so far (for warm-up)
public:
   void              Init(const double duration = 40.0, const double ssPeriod = 10.0)
     {
      //--- single-pole high-pass (book listing 12-1)
      double angle = 2.0 * EHLERS_PI / duration;      // 360/Duration deg
      m_alpha1 = (1.0 - MathSin(angle)) / MathCos(angle);
      //--- super smoother coefficients
      double a1 = MathExp(-1.414 * EHLERS_PI / ssPeriod);
      double b1 = 2.0 * a1 * MathCos(1.414 * EHLERS_PI / ssPeriod);
      m_c2 = b1;
      m_c3 = -a1 * a1;
      m_c1 = 1.0 - m_c2 - m_c3;
      m_price.Init(2);
      m_hp.Init(2);
      m_filt.Init(3);
      m_wave = 0.0;
      m_count = 0;
     }
   //--- feed raw price; returns the normalized sinewave value
   double            Update(const double price)
     {
      double hp;
      if(m_count < 1)
         hp = 0.0;
      else
         hp = 0.5 * (1.0 + m_alpha1) * (price - m_price[0]) + m_alpha1 * m_hp[0];
      m_price.Push(price);
      m_hp.Push(hp);

      double filt;
      if(m_count < 2)
         filt = hp;
      else
         filt = m_c1 * (hp + m_hp[1]) / 2.0 + m_c2 * m_filt[0] + m_c3 * m_filt[1];
      m_filt.Push(filt);

      if(m_filt.Ready(3))
        {
         double wave = (m_filt[0] + m_filt[1] + m_filt[2]) / 3.0;
         double pwr  = (m_filt[0] * m_filt[0] + m_filt[1] * m_filt[1] + m_filt[2] * m_filt[2]) / 3.0;
         m_wave = (pwr > 0.0 ? wave / MathSqrt(pwr) : 0.0);
        }
      m_count++;
      return(m_wave);
     }
   double            Value() const { return(m_wave); }
   bool              Ready() const { return(m_count >= 3); }
  };

Проверка pwr > 0.0 перед делением проста, но необходима: на совершенно плоском участке все отфильтрованные значения и мощность равны нулю, а деление дало бы "нечисло" (NaN), которое в рекурсивном фильтре исказило бы все последующие значения. Возврат нуля в этом вырожденном случае обеспечивает корректное поведение результата. Это тот же принцип защитного программирования, что и в ветвях обработки прогрева: опубликованные листинги Элерса рассчитаны на среду, которая молча допускает такие граничные случаи, но при аккуратном переносе на MQL5 оставлять их без обработки не следует.

Оболочка индикатора почти полностью повторяет оболочку Roofing Filter; линии уровней +0.85, 0 и -0.85 позволяют сразу заметить удержание у предельного уровня. Единственное отличие, на которое стоит обратить внимание, — это использование функции Ready() при заполнении буфера.

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
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[])
  {
   if(rates_total < 8)
      return(0);

   g_ebsw.Init(InpDuration, InpSSPeriod);
   for(int i = 0; i < rates_total; i++)
     {
      double price = (high[i] + low[i]) / 2.0;
      double v = g_ebsw.Update(price);
      EbswBuffer[i] = (g_ebsw.Ready() ? v : EMPTY_VALUE);
     }
   return(rates_total);
  }

Отличие от индикатора Roofing Filter заключается в использовании выражения g_ebsw.Ready()? v: EMPTY_VALUE при присваивании значения элементу буфера. Пока фильтр не получит три отсчета, осмысленного значения волны нет, а запись EMPTY_VALUE указывает терминалу ничего не рисовать на этом участке, чтобы во время прогрева не отображался вводящий в заблуждение горизонтальный отрезок. Индикатору Roofing Filter это не требуется, поскольку его ветвь обработки прогрева уже возвращает постоянное нулевое значение, которое корректно интерпретируется как "колебаний пока нет".

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

Even Better Sinewave: колебания в диапазоне с последующим удержанием у предельного уровня во время тренда

Рис. 5. Осциллятор Even Better Sinewave. Он колеблется между линиями уровней, пока рынок находится в циклическом режиме, а когда начинает преобладать тренд, удерживается у предельного уровня около +1 или -1. Этот переход служит сигналом смены рыночного режима с циклического на трендовый.


Заключение

Мы поставили перед собой задачу последовательно применить подход Элерса к цене как к сигналу и реализовать его в виде рабочего кода на MQL5, пригодного для повторного использования. Мы исходили из того, что ценовой график представляет собой сигнал, содержащий шум, цикл, пригодный для торговли, и медленный тренд, а фильтры — подходящий инструмент для разделения этих составляющих. Мы создали небольшую библиотеку, архитектура которой позволяет использовать одни и те же математические вычисления как для индикаторов, так и, в дальнейшем, для советника без дублирования кода. В этой библиотеке мы реализовали Super Smoother — фильтр нижних частот (ФНЧ) с малым запаздыванием, превосходящий по эффективности заменяемую им скользящую среднюю; Roofing Filter, в котором каскадное соединение фильтра верхних частот (ФВЧ) и Super Smoother позволяет выделить диапазон периодов, пригодный для торговли; а также Even Better Sinewave — циклический осциллятор, у которого удержание у предельного уровня превращает обычный недостаток колебательных индикаторов в средство обнаружения трендового режима. При этом мы уделяли особое внимание тому, что обычно остается за рамками опубликованных листингов: переходным процессам при прогреве, защите от деления на ноль и корректной работе с рекурсивными фильтрами с бесконечной импульсной характеристикой (БИХ-фильтрами), сохраняющими внутреннее состояние, в OnCalculate.

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


Файл
Тип
Описание
EhlersDSP.mqh
Библиотека
Полная библиотека классов цифровой обработки сигналов (ЦОС): SignalHistory, SuperSmoother, RoofingFilter, CyclePeriod, MAMA и EvenBetterSinewave.
EhlersRoofingFilter.mq5
Индикатор
Roofing Filter (каскадное соединение двухполюсного фильтра верхних частот (ФВЧ) и Super Smoother) отображается в отдельном подокне.
EhlersEvenBetterSinewave.mq5
Индикатор
Осциллятор Even Better Sinewave с линиями уровней, показывающий циклические колебания и удержание у предельного уровня в трендовом режиме.

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/23149

Прикрепленные файлы |
MQL5.zip (7.65 KB)
Низкочастотные количественные стратегии в MetaTrader 5 (Часть 3): Система свинг-трейдинга на основе возврата к среднему значению с адаптацией к рыночным режимам Низкочастотные количественные стратегии в MetaTrader 5 (Часть 3): Система свинг-трейдинга на основе возврата к среднему значению с адаптацией к рыночным режимам
В статье описывается и приводится код стратегии MR Swing на языке MQL5 — это подход к свинг-трейдингу на основе возврата к среднему значению, сочетающий 200-дневный канал гистерезиса с индикаторами Value Chart (график стоимости), DVO и SVAPO. Мы описываем правила входа в позиции и выхода из них для бычьего и медвежьего режимов и приводим результаты пятилетнего бэктестирования на шести высоколиквидных акциях, котирующихся на Nasdaq. Для обеспечения воспроизводимости результатов предоставляются полный код советника и настройки бэктестирования.
Интерактивное управление зонами спроса и предложения на MQL5 (Часть II): Событийно-ориентированная архитектура и журналирование жизненного цикла с долговременным хранением Интерактивное управление зонами спроса и предложения на MQL5 (Часть II): Событийно-ориентированная архитектура и журналирование жизненного цикла с долговременным хранением
В этой статье мы совершенствуем систему управления зонами спроса и предложения с отслеживаемым состоянием для MetaTrader 5, заменяя опрос событийно-ориентированной моделью на основе OnChartEvent(). Мы распределяем задачи синхронизации между отдельными обработчиками создания, изменения и удаления, а также отделяем рыночную логику в OnTick() от обработки действий пользователя в OnChartEvent(). Компонент журналирования в формате CSV с долговременным хранением и режимом только с добавлением записей фиксирует все события жизненного цикла, улучшая скорость отклика, согласованность состояний и возможность восстановления истории для последующего анализа.
Конструирование признаков для машинного обучения (Часть 9): Tесты на структурные сдвиги на Python Конструирование признаков для машинного обучения (Часть 9): Tесты на структурные сдвиги на Python
Мы представляем реализацию тестов на структурные сдвиги из главы 17 AFML, готовую для использования в реальных системах. Модуль включает тест Чу — Стинчкомба — Уайта (односторонний и двусторонний варианты), тест Дики — Фуллера типа Чоу (DFC), SADF для шести моделей (линейная, квадратичная, sm poly 1, sm poly 2, sm exp, sm power), а также квантильную статистику ADF (QADF) с параметрами q и v и условное среднее ADF (CADF) с параметром q; результаты возвращаются в виде скалярных признаков с индексацией по барам. Мы устраняем проблемы масштабируемости фрагментов кода из книги и ошибку, связанную с порядком аргументов, а также показываем, как фиксированная глубина ретроспективы (L=504) ограничивает вычислительную сложность SADF величиной O(L²) на бар при выявлении рыночных режимов.
Персистентное хранилище "ключ — значение" в MQL5: Плоские файлы как легковесная база данных для хранения состояния советника Персистентное хранилище "ключ — значение" в MQL5: Плоские файлы как легковесная база данных для хранения состояния советника
Благодаря облегченной архитектуре системы сохранения данных торговые советники (EA) могут сохранять счетчики, флаги и временные метки между перезапусками терминала. Используя только средства MQL5, CPersistentStore записывает удобочитаемый файл формата key=value в папку MQL5/Files и обслуживает запросы на чтение из кэша со сквозной записью на основе CHashMap через типизированный API. В статье анализируются операции со сложностью O(1) и O(n), риски неполной записи и отсутствие блокировки файлов, проводится сравнение с глобальными переменными терминала (GlobalVariables) и SQLite, а также приводится демонстрационный пример с детерминированной повторной загрузкой состояния.