English
preview
Ценовые каналы в MQL5 без предположений о форме распределения: Квантильная регрессия методом итеративно перевзвешенных наименьших квадратов (IRLS)

Ценовые каналы в MQL5 без предположений о форме распределения: Квантильная регрессия методом итеративно перевзвешенных наименьших квадратов (IRLS)

MetaTrader 5 — Машинное обучение |
16 0
Adedayo David Gbadebo
Adedayo David Gbadebo

Содержание

  1. Введение
  2. От метода наименьших квадратов к квантилям
  3. Построение квантильной прямой методом IRLS
  4. Создание канала
  5. Индикатор канала
  6. Индикатор ширины канала
  7. Действительно ли эти прямые дают оценки квантилей?
  8. Ширина канала как признак волатильности
  9. Проверка в тестере стратегий
  10. Заключение


Введение

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

В этой статье мы создадим канал, который не основывается ни на одном из этих допущений. Вместо того чтобы оценивать среднее значение в окне и измерять стандартное отклонение, мы непосредственно строим три условные квантильные прямые: они описывают квантили цены уровней 0.1, 0.5 и 0.9 как функции положения в окне. Каждая прямая строится независимо путем минимизации квантильной функции потерь (pinball loss) с помощью компактной процедуры, реализующей метод итеративно перевзвешенных наименьших квадратов (IRLS). В результате получается канал, не требующий предположений о распределении: при его построении не вычисляется стандартное отклонение и не предполагается нормальность распределения, а сам канал может быть асимметричным, если асимметричны данные. Вы узнаете, как выполняется такая аппроксимация и как реализовать ее в виде класса для многократного использования и двух индикаторов MetaTrader 5. Затем мы проведем тестирование: проверим, действительно ли три прямые делят данные в окне в пропорциях, соответствующих их квантильным уровням; сравним ширину канала с шириной полос Боллинджера, средним истинным диапазоном (ATR) и шириной регрессионного канала при одной и той же глубине расчета; а также посмотрим, как намеренно наивный советник ведет себя в тестере стратегий на четырех инструментах.


От метода наименьших квадратов к квантилям

Обычный метод наименьших квадратов (МНК, OLS) строит прямую, минимизируя сумму квадратов остатков. Полученная прямая проходит через условное среднее данных: для любого положения на оси x значение на построенной прямой соответствует математическому ожиданию y в этой точке. Возведение остатков в квадрат привязывает аппроксимацию к среднему значению, а также делает метод OLS чувствительным к выбросам, поскольку один-единственный большой остаток, возведённый в квадрат, доминирует в сумме.

Квантильная регрессия заменяет квадрат остатка другой штрафной функцией, которая привязывает аппроксимацию к выбранному квантилю, а не к среднему значению. Для целевого квантиля уровня tau (числа от 0 до 1) штраф за остаток r задается квантильной функцией потерь (pinball loss), также известной как check loss:

rho_tau(r) = tau * r if r >= 0, and (tau - 1) * r if r < 0.

Вся суть метода заключается именно в этой асимметрии. У точки выше прямой остаток положителен, и штраф за него имеет вес tau; у точки ниже прямой остаток отрицателен, и штраф за него имеет вес (1 - tau). При tau = 0.5 оба веса равны, поэтому минимизация квантильной функции потерь сводится к минимизации суммы модулей остатков, решение которой соответствует медиане. При tau = 0.9 положительный остаток штрафуется с весом 0.9, а отрицательный — всего с весом 0.1, поэтому аппроксимирующая прямая смещается вверх, пока примерно 90 % точек не окажутся ниже нее. Именно это и является определением условного квантиля 0.9.

Квантильная функция потерь для трех квантильных уровней

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

Для ценового канала важны три свойства данной конструкции:

  • Это канал, не требующий предположений о распределении. Нормальность распределения не предполагается, и стандартное отклонение не вычисляется. Квантиль — свойство самих данных, независимо от формы их распределения.
  • Он устойчив к выбросам. Функция потерь зависит от остатка линейно, а не квадратично, поэтому один экстремальный бар не может оказывать определяющее влияние на аппроксимацию.
  • Границы независимы друг от друга. Квантильные прямые уровней 0.1 и 0.9 строятся независимо друг от друга. Ничто не требует их симметричного расположения относительно медианы, поэтому канал может быть шире с одной стороны, чем с другой.

Плата за все эти преимущества — отсутствие решения в явном виде для минимума квантильной функции потерь, в отличие от обычного метода наименьших квадратов (МНК, OLS). Нет формулы на основе нормальных уравнений, которая позволяла бы получить квантильную прямую за один шаг. Именно здесь на помощь приходит IRLS.


Построение квантильной прямой с помощью метода итеративно перевзвешенных наименьших квадратов (IRLS)

Квантильная функция потерь не является гладкой: ее график имеет излом в точке r = 0, где угловой коэффициент скачком меняется с (tau - 1) на tau; этот излом виден в начале координат на рис. 1. При минимизации нельзя просто приравнять производную к нулю, поскольку она не определена именно в тех точках, которые нас интересуют. Метод итеративно перевзвешенных наименьших квадратов (IRLS) позволяет обойти эту проблему, сводя минимизацию неудобной функции потерь к последовательности стандартных задач взвешенного метода наименьших квадратов, каждая из которых действительно имеет решение в явном виде.

Идея заключается в том, что квантильную функцию потерь можно записать в виде взвешенного квадрата остатка, если допустить зависимость веса от самого остатка. Присвойте каждой точке вес квантильной функции потерь (tau для точек выше прямой, 1 - tau для точек ниже), деленный на абсолютную величину ее остатка. Постройте прямую, используя взвешенный метод наименьших квадратов, пересчитайте остатки относительно новой прямой, обновите веса и повторите процедуру. На каждой итерации значение квантильной функции потерь уменьшается, а положение прямой стабилизируется. Это вариант квантильной регрессии на основе IRLS по схеме Шлоссмахера: Шлоссмахер (1973) опубликовал эту схему для метода наименьших абсолютных отклонений, соответствующего случаю tau = 0.5, а приведенные выше асимметричные веса обобщают ее на любое значение tau.

Весь метод реализован в одном классе — CQuantileLine — в файле QuantileRegression.mqh. Этот класс представляет одну квантильную прямую y = a + b*x, где x — центрированная позиция бара внутри окна. Конструктор ограничивает значение tau открытым интервалом (0, 1) и сохраняет параметры, необходимые для цикла IRLS: предельное число итераций, допуск сходимости и эпсилон, задающий нижнюю границу модуля остатка:

//--- construct for a given quantile with sensible IRLS controls
                     CQuantileLine(const double tau = 0.5,
                                   const int maxIter = 200,
                                   const double tol = 1e-6,
                                   const double eps = 1e-6)
     {
      m_tau     = (tau <= 0.0 ? 1e-6 : (tau >= 1.0 ? 1.0 - 1e-6 : tau));
      m_maxIter = (maxIter < 1 ? 1 : maxIter);
      m_tol     = tol;
      m_eps     = eps;
      m_a       = 0.0;
      m_b       = 0.0;
      m_valid   = false;
      m_iters   = 0;
     }

На каждой итерации IRLS вызывается процедура решения задачи взвешенного метода наименьших квадратов: она использует решение в явном виде системы нормальных уравнений размером 2×2 с проверкой на вырожденность.

//--- Weighted least-squares solve of y = a + b*x for the given
//--- per-point weights. Returns false on a degenerate (singular)
//--- normal-equation system, leaving a,b untouched.
   bool              SolveWLS(const double &x[], const double &y[],
                              const double &w[], const int n,
                              double &a, double &b) const
     {
      double sw = 0.0, swx = 0.0, swy = 0.0, swxx = 0.0, swxy = 0.0;
      for(int i = 0; i < n; i++)
        {
         double wi = w[i];
         sw   += wi;
         swx  += wi * x[i];
         swy  += wi * y[i];
         swxx += wi * x[i] * x[i];
         swxy += wi * x[i] * y[i];
        }
      double det = sw * swxx - swx * swx;
      if(MathAbs(det) < 1e-12)
         return(false);
      b = (sw * swxy - swx * swy) / det;
      a = (swy - b * swx) / sw;
      return(true);
     }

Если все веса равны 1.0, задача сводится к обычному методу наименьших квадратов (МНК, OLS), который и используется для начального оценивания. Если определитель близок к нулю, проверка возвращает false, не допуская деления на почти нулевое число, поэтому для вырожденного окна, например последовательности одинаковых цен, расчет корректно завершается с ошибкой, а не выдает некорректные данные.

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

//--- fit the quantile line to y[] sampled at centred positions x[].
//--- x[] and y[] must both hold n points; returns true on success.
//--- Convergence is RELATIVE: the movement of the fitted value at the
//--- window edge against its height there, so it is scale-free.
   bool              Fit(const double &x[], const double &y[], const int n)
     {
      m_valid = false;
      m_iters = 0;
      if(n < 2)
         return(false);

      //--- seed from an ordinary (unit-weight) least-squares fit
      double w[];
      ArrayResize(w, n);
      for(int i = 0; i < n; i++)
         w[i] = 1.0;

      double a, b;
      if(!SolveWLS(x, y, w, n, a, b))
         return(false);

      //--- distance from the window centre out to the read-out edge
      double xEdge = MathAbs(x[n - 1]);

      //--- IRLS: reweight by the pinball loss, refit, until settled
      for(int it = 0; it < m_maxIter; it++)
        {
         for(int i = 0; i < n; i++)
           {
            double r = y[i] - (a + b * x[i]);       // signed residual
            //--- pinball weight: tau above the line, 1-tau below,
            //--- divided by |r| so weighted LS mimics the L1-type loss
            double side = (r >= 0.0 ? m_tau : 1.0 - m_tau);
            double ar   = MathAbs(r);
            if(ar < m_eps)
               ar = m_eps;                           // guard the kink at r=0
            w[i] = side / ar;
           }

         double na, nb;
         if(!SolveWLS(x, y, w, n, na, nb))
            return(false);

         //--- edge movement, scored against the line's height there
         double move  = MathAbs(na - a) + MathAbs(nb - b) * xEdge;
         a = na;
         b = nb;
         m_iters = it + 1;
         double scale = MathAbs(a) + MathAbs(b) * xEdge;
         if(scale > 0.0 && move < m_tol * scale)
            break;
        }

      m_a     = a;
      m_b     = b;
      m_valid = true;
      return(true);
     }

Стоит остановиться на двух деталях. Строка if(ar < m_eps) ar = m_eps; обеспечивает защиту в точке излома: у точки, лежащей точно на прямой, остаток равен нулю, и деление на него привело бы к бесконечному весу. Ограничение модуля остатка снизу малым значением эпсилон сохраняет вес конечным и обеспечивает устойчивость итерационного процесса.

Второй момент касается проверки сходимости: очевидное правило остановки оказывается неверным. Инстинктивно хочется остановиться, когда коэффициенты перестают изменяться: вычислить move = |da| + |db| и сравнить это значение с небольшим фиксированным числом. Именно так этот класс работал в одной из предыдущих версий с допуском 1e-7, и результаты никогда не вызывали подозрений.

На практике эта проверка требует, чтобы изменение свободного члена было меньше 1e-7 в абсолютных ценовых единицах. Для EURUSD при цене около 1.10 это жесткое, но выполнимое требование. Для BTCUSD при цене около 20 000 та же константа требует относительной точности около 5e-12, которой метод с линейной сходимостью не достигнет за разумное число итераций. Следствием стал не неверный результат, а незаметная проблема: цикл в каждом окне доходил до предельного числа итераций, но, поскольку аппроксимация становилась достаточно хорошей задолго до этого, никаких признаков неполадки не было. На дневной истории BTCUSD предельное число итераций было достигнуто в 2 238 из 2 258 окон; тот же код на EURUSD достигал предела в 77 % окон вместо 99 %, и именно это различие между двумя инструментами при одинаковом коде выдает проблему.

Решение — использовать относительный критерий сходимости. В приведенном выше варианте измеряется изменение значения аппроксимирующей прямой на краю окна, которое затем нормируется на модуль значения прямой в этой точке. Обе величины выражены в ценовых единицах, поэтому их отношение безразмерно и имеет один и тот же смысл для любого торгового инструмента. Измерять следует на правом краю окна, поскольку именно значение в этой точке индикаторы считывают и отображают на графике. При использовании этого критерия с допуском 1e-6 медианное число итераций до сходимости одной квантильной прямой составляет примерно 40–45, поэтому предельное число итераций было увеличено с 50 до 200. Для построения канала оцениваются три такие прямые, а сходимость канала определяется самой медленно сходящейся из них, поэтому приведенные далее числа итераций для канала в целом еще выше.

Остальные члены класса — небольшие методы доступа, позволяющие получить значения построенной прямой:

//--- fitted value at a centred position x (a + b*x)
   double            ValueAt(const double x) const { return(m_a + m_b * x); }
   double            Intercept()             const { return(m_a); }
   double            Slope()                 const { return(m_b); }
   double            Tau()                   const { return(m_tau); }
   bool              IsValid()               const { return(m_valid); }

//--- IRLS passes the last Fit() used; equals the cap when the
//--- tolerance was never met.
   int               Iterations()            const { return(m_iters);   }
   int               MaxIterations()         const { return(m_maxIter); }

Именно метод Iterations() позволил выявить эту проблему: без него нельзя отличить окно, для которого сходимость достигнута за двенадцать итераций, от окна, для которого расчет остановился после двухсот итераций без достижения сходимости, поскольку в обоих случаях возвращается правдоподобная прямая. Метод ValueAt(x) возвращает значение прямой в любой точке; поскольку начало координат x находится в середине окна, ValueAt(0) возвращает значение в центре, а значение на правом краю соответствует последнему бару.


Создание канала

Одна квантильная прямая — это лишь половина дела. Канал состоит из трех таких прямых, построенных по данным одного и того же окна: нижнего квантиля, медианы и верхнего квантиля. Класс CQuantileChannel содержит три объекта CQuantileLine и управляет ими одновременно. Его конструктор принимает одно значение вероятности в хвосте распределения и преобразует его в три значения tau в списке инициализаторов членов класса. Так, значение 0.1 задает канал 0.1 / 0.5 / 0.9: нижняя прямая соответствует tau = 0.1, медиана фиксирована на уровне 0.5, а верхняя прямая соответствует tau = 1 − 0.1 = 0.9:

//--- construct the channel for a symmetric quantile pair.
//--- tailProb is the tail mass on each side, so 0.1 gives the
//--- 0.1 / 0.5 / 0.9 channel; the median is always fitted.
                     CQuantileChannel(const double tailProb = 0.1,
                                      const int maxIter = 200) :
                     m_low(tailProb, maxIter),
                     m_mid(0.5, maxIter),
                     m_high(1.0 - tailProb, maxIter)
     {
      m_xRight = 0.0;
      m_valid  = false;
     }

Метод `Fit` создаёт ось x один раз и передаёт её всем трём прямым. Вместо использования исходных индексов баров 0, 1, 2 и так далее мы центрируем их относительно середины окна, так что x принимает значения от -(n-1)/2 до +(n-1)/2. Центрирование сохраняет небольшие числовые значения и хорошую обусловленность системы нормальных уравнений, а также делает свободный член каждой прямой равным ее значению в центре окна. Последний бар находится в точке x = (n-1) - midpoint; эта координата сохраняется в m_xRight для методов считывания значений.

//--- fit all three lines to the price window y[0..n-1], where
//--- y[0] is the oldest bar in the window and y[n-1] the newest.
//--- Returns true only if every line fitted successfully.
   bool              Fit(const double &y[], const int n)
     {
      m_valid = false;
      if(n < 2)
         return(false);

      //--- centred x-axis: -(n-1)/2 .. +(n-1)/2, newest bar on the right
      double x[];
      ArrayResize(x, n);
      double mid = (n - 1) / 2.0;
      for(int i = 0; i < n; i++)
         x[i] = i - mid;
      m_xRight = (n - 1) - mid;

      bool ok = m_low.Fit(x, y, n);
      ok = m_mid.Fit(x, y, n) && ok;
      ok = m_high.Fit(x, y, n) && ok;

      m_valid = ok;
      return(ok);
     }

Результат построения считается корректным только при успешном построении всех трех прямых. Затем методы считывания вычисляют значение каждой прямой на правом краю окна. Второй набор методов доступа ...At(x) вычисляет значения этих прямых в любой точке внутри окна. Это позволяет при анализе сопоставлять с построенной по данным этого окна прямой каждую точку окна, а не только точку на его правом краю:

//--- channel read-out at the most recent bar (right window edge)
   double            Lower()  const { return(m_low.ValueAt(m_xRight));  }
   double            Median() const { return(m_mid.ValueAt(m_xRight));  }
   double            Upper()  const { return(m_high.ValueAt(m_xRight)); }

//--- channel read-out at any centred position x inside the window.
//--- x=0 is the window centre, x=m_xRight the most recent bar. Used
//--- to check in-sample coverage across the whole fitted window.
   double            LowerAt(const double x)  const { return(m_low.ValueAt(x));  }
   double            MedianAt(const double x) const { return(m_mid.ValueAt(x));  }
   double            UpperAt(const double x)  const { return(m_high.ValueAt(x)); }

Помимо этих трёх уровней, класс предоставляет две производные величины. Spread — ширина канала, то есть вертикальное расстояние между верхней и нижней прямыми; это не требующий предположений о распределении показатель разброса цены вокруг ее тренда. Асимметрия канала определяется как разность верхней полуширины (верхняя граница минус медиана) и нижней полуширины (медиана минус нижняя граница), нормированная на общую ширину канала, поэтому принимает значения от -1 до +1. Поскольку обе границы строятся независимо друг от друга, асимметрия канала может быть ненулевой: она положительна, когда верхняя половина канала шире, и отрицательна, когда шире нижний хвост. Это свойство самого способа построения, а не навязанное нами условие.

//--- dispersion and asymmetry read-outs at the most recent bar
   double            Spread()   const { return(Upper() - Lower());   }
   double            UpperGap() const { return(Upper() - Median());  }
   double            LowerGap() const { return(Median() - Lower());  }

//--- skew of the channel: >0 when the upper half is the wider one,
//--- <0 when the downside carries the fatter tail. In [-1, +1].
//--- A crossed channel returns 0 here; use Crossed() to tell that
//--- apart from a genuinely symmetric window.
   double            Skew() const
     {
      double s = Spread();
      if(s <= 0.0)
         return(0.0);
      return((UpperGap() - LowerGap()) / s);
     }

Эта проверка на неположительное значение Spread (ширины канала) — не просто шаблонная защитная мера: именно здесь проявляется реальная проблема построения канала. Все три квантильные прямые строятся независимо друг от друга, и ничто не гарантирует сохранения их порядка, поэтому прямая квантиля 0.9 может оказаться ниже прямой квантиля 0.5. Это проблема пересечения квантильных прямых — плата за независимость их построения, благодаря которой канал может быть асимметричным. Границы симметричной полосы не могут пересечься, поскольку задаются отступами от одной центральной линии; границы нашего канала могут пересекаться именно потому, что строятся иначе. Пересечение проявляется на краях окна, где прямые с разными угловыми коэффициентами расходятся сильнее всего и где считываются значения. Возврат значения 0 из Skew() предотвращает арифметические ошибки, но для симметричного канала эта функция тоже возвращает 0, поэтому одна лишь такая проверка выдавала бы некорректный результат построения за правдоподобное значение показателя. Поэтому канал предоставляет прямой доступ к признаку этого состояния:

//--- The lines are fitted independently, so they can cross. A crossed
//--- window is a broken fit rather than a signal: sit it out.
   bool              Crossed() const
     {
      return(Lower() > Median() || Median() > Upper());
     }

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


Индикатор канала

Поскольку логика канала вынесена в подключаемый файл, индикатору для его отрисовки требуется немного кода. В файле QuantileChannel.mq5 объявляются три буфера индикатора, по одному на каждую прямую, а их внешний вид настраивается в блоке свойств в начале файла. Здесь показаны настройки нижней линии; для медианной и верхней линий задаются те же пять свойств, причем толщина медианной линии равна 2, чтобы она воспринималась как линия тренда:

#property indicator_chart_window
#property indicator_buffers 3
#property indicator_plots   3

#property indicator_label1  "Lower"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrTomato
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1

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

input int    InpWindow   = 100;             // Rolling window length (bars)
input double InpTailProb = 0.1;             // Tail probability each side (0.1 -> 0.1/0.5/0.9)
input ENUM_APPLIED_PRICE InpPrice = PRICE_CLOSE; // Applied price

double         LowerBuffer[];               // fitted lower quantile line
double         MedianBuffer[];              // fitted median line
double         UpperBuffer[];               // fitted upper quantile line

Функция OnInit привязывает три буфера и задает краткое имя индикатора, включающее значения параметров и отображаемое в заголовке окна данных. Особого внимания заслуживают заключительные строки этой функции: для первых InpWindow - 1 баров еще нет полного окна данных, поэтому установка PLOT_EMPTY_VALUE в EMPTY_VALUE оставляет их без линии, вместо того чтобы проводить ее к нулю; длина окна менее двух баров сразу отклоняется:

//--- warm-up bars carry no channel yet; keep them off the plot
   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetDouble(2, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   return(InpWindow < 2 ? INIT_PARAMETERS_INCORRECT : INIT_SUCCEEDED);

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

//+------------------------------------------------------------------+
//| Pull the chosen applied price for bar i into a plain array.      |
//+------------------------------------------------------------------+
double AppliedPrice(const int i,
                    const double &open[], const double &high[],
                    const double &low[], const double &close[])
  {
   switch(InpPrice)
     {
      case PRICE_OPEN:     return(open[i]);
      case PRICE_HIGH:     return(high[i]);
      case PRICE_LOW:      return(low[i]);
      case PRICE_MEDIAN:   return((high[i] + low[i]) / 2.0);
      case PRICE_TYPICAL:  return((high[i] + low[i] + close[i]) / 3.0);
      case PRICE_WEIGHTED: return((high[i] + low[i] + 2.0 * close[i]) / 4.0);
      default:             return(close[i]);
     }
  }

Основная логика индикатора реализована в OnCalculate: для каждого бара, требующего расчета, загружается окно из InpWindow цен, заканчивающееся на этом баре, строится канал, а три его уровня сохраняются в индикаторных буферах. Повторное построение канала по полному окну на каждом историческом баре требует вычислительных затрат, поэтому prev_calculated используется, чтобы не пересчитывать неизменившиеся бары. При первом вызове значение prev_calculated равно 0, и start указывает на первый бар, для которого уже доступно полное окно данных; при последующих вызовах prev_calculated содержит значение, возвращенное предыдущим вызовом, поэтому расчет возобновляется с последнего рассчитанного бара. Окно загружается в порядке от самого старого к самому новому, чтобы соответствовать отцентрированной оси x внутри CQuantileChannel, где индекс 0 соответствует самому старому бару. Если построить канал по данным окна не удалось, в буферы записывается EMPTY_VALUE:

//+------------------------------------------------------------------+
//| 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 < InpWindow)
      return(0);

   CQuantileChannel channel(InpTailProb);
   double win[];
   ArrayResize(win, InpWindow);

//--- only recompute bars that are new since the last call; each bar's
//--- channel is the fit over the InpWindow prices ending at that bar.
   int start = (prev_calculated > InpWindow ? prev_calculated - 1 : InpWindow - 1);
   for(int i = start; i < rates_total; i++)
     {
      //--- load the window ending at bar i (oldest first, newest last)
      for(int k = 0; k < InpWindow; k++)
         win[k] = AppliedPrice(i - InpWindow + 1 + k, open, high, low, close);

      if(channel.Fit(win, InpWindow))
        {
         LowerBuffer[i]  = channel.Lower();
         MedianBuffer[i] = channel.Median();
         UpperBuffer[i]  = channel.Upper();
        }
      else
        {
         LowerBuffer[i]  = EMPTY_VALUE;
         MedianBuffer[i] = EMPTY_VALUE;
         UpperBuffer[i]  = EMPTY_VALUE;
        }
     }
   return(rates_total);
  }

При добавлении индикатора на график три линии образуют канал вокруг цены.

Квантильный канал и индикатор его ширины на ценовом графике

Рис. 2. Квантильный канал на ценовом графике; ниже — индикатор ширины и асимметрии канала.


Индикатор ширины канала

Второй индикатор, QuantileChannelGauge.mq5, отображает ширину и асимметрию канала не на ценовом графике, а в отдельном окне. Он использует те же входные параметры и ту же вспомогательную функцию AppliedPrice, что и индикатор канала, поэтому здесь показаны только отличающиеся части его реализации. Его блок свойств имеет ту же структуру, что и у индикатора канала, отличаясь лишь объявлением indicator_separate_window и двух буферов — Spread и Skew — вместо трёх.

Его функция OnInit отличается в одном аспекте: показатель асимметрии канала колеблется вокруг нуля, поэтому проводится нулевая линия отсчета, чтобы знак значения был понятен с первого взгляда.

//--- a zero reference line makes the Skew sign readable at a glance
   IndicatorSetInteger(INDICATOR_LEVELS, 1);
   IndicatorSetDouble(INDICATOR_LEVELVALUE, 0, 0.0);

Цикл вычислений устроен так же, как у индикатора канала, но сохраняет два производных показателя вместо трёх уровней:

if(channel.Fit(win, InpWindow))
        {
         SpreadBuffer[i] = channel.Spread();
         SkewBuffer[i]   = channel.Skew();
        }
      else
        {
         SpreadBuffer[i] = EMPTY_VALUE;
         SkewBuffer[i]   = EMPTY_VALUE;
        }


Действительно ли эти линии представляют собой оценки квантилей?

До сих пор мы занимались только реализацией. Мы назвали три прямые условными квантилями уровней 0.1, 0.5 и 0.9 и реализовали алгоритм, который должен их строить, но само название еще не гарантирует соответствия этим квантилям. Прежде чем выяснять, для чего полезен этот канал, следует проверить, действительно ли он обладает заявленными свойствами.

Проверка проста. Если верхняя прямая действительно представляет собой условный квантиль цены уровня 0.9 в данном окне, то около 90 % цен в этом окне должны находиться ниже нее: при этом каждую цену сравнивают со значением прямой в соответствующей позиции, а не на правом краю окна, — именно поэтому класс предоставляет методы ...At(x). Скрипт QC_Validation.mq5 выполняет такой подсчет в скользящем окне по истории нескольких инструментов и выводит результат. Ниже приведены результаты для базовой конфигурации — окна длиной 100 баров с tailProb = 0.1 — на четырех инструментах из четырех классов активов:

Инструмент
Ниже нижней границы (целевая доля — 10 %)
Ниже медианы (целевая доля — 50 %)
Ниже верхней границы (целевая доля — 90 %)
BTCUSD, D1
10.8 %
50.0 %
89.2 %
EURUSD, D1
10.8 %
50.0 %
89.2 %
XAUUSD, D1
10.8 %
50.0 %
89.2 %
SPX500, D1
10.8 %
50.0 %
89.2 %

Построенный канал обладает заявленными свойствами, а показатели не просто близки к целевым значениям, но и одинаковы на четырех не связанных между собой рынках. Это следствие комбинаторики, а не случайное совпадение. Квантильная регрессия накладывает точное ограничение на результат: построенная прямая проходит через столько точек данных, сколько у нее параметров, а доля точек ниже нее отличается от целевого значения не более чем на p/n, где p — число параметров (свободный член и угловой коэффициент), а n — длина окна. При n = 100 эта граница составляет два процентных пункта, а число точек ниже прямой — целое число от 0 до 100, поэтому возможные значения доли дискретны независимо от того, что происходит на рынке.

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

Окно
Ниже нижней границы
Отклонение от 10 %
Граница p/n
50 баров
11.7 %
1.7 пункта
4.0 %
100 баров
10.8 %
0.8 пункта
2.0 %
200 баров
10.4 %
0.4 пункта
1.0 %

Отклонение уменьшается вдвое при каждом удвоении размера окна, в точном соответствии с оценкой его предельной величины, и во всех случаях остается с запасом в пределах этой оценки. Изменение вероятности в хвосте распределения столь же точно смещает прямые к новым целевым уровням: при tailProb = 0.05 охват составляет 5.9 / 50.0 / 94.1 при целевых значениях 5 / 50 / 95, а при tailProb = 0.20 — 20.6 / 50.0 / 79.4 при целевых значениях 20 / 50 / 80.

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

Тот же тест, на один бар вперед

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

Инструмент
Закрытие следующего бара ниже нижней границы
Закрытие следующего бара ниже медианы
Закрытие следующего бара ниже верхней границы
Всего вне канала
BTCUSD, D1
20.9 %
45.7 %
73.1 %
47.8 %
EURUSD, D1
21.9 %
49.7 %
79.3 %
42.6 %
XAUUSD, D1
20.4 %
49.0 %
75.6 %
44.8 %
SPX500, D1
22.6 %
52.4 %
74.2 %
48.4 %

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

Причина не в недостатках аппроксимации, а в различии между описанием и прогнозированием. Этот канал прекрасно описывает данные окна, по которым он был построен. Однако цену нельзя считать случайным наблюдением из неизменного распределения этого окна: следующий бар имеет собственный дрейф, а построенные границы канала экстраполируются до самого края диапазона аппроксимации, где любая линейная аппроксимация наименее надежна. Толкование верхней линии как «завтра цена превысит этот уровень лишь в 10 % случаев» не подтверждается данными.

Рассматривайте этот канал как описание, а не как прогноз. Эти прямые действительно представляют собой условные квантили данных окна, по которым они были построены; проверка на четырех классах активов подтвердила это в пределах теоретической границы погрешности. Они не являются прогнозными границами для следующего бара: примерно 45 % следующих баров закрываются за пределами канала, номинально рассчитанного на охват 80 % таких баров. Любое правило, трактующее касание верхней прямой как редкое событие, более чем вдвое занижает вероятность такого события.

Вычислительные затраты на аппроксимацию

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

Инструмент
Окна
Среднее число итераций IRLS (максимум по трем прямым)
Достигнуто предельное число итераций — 200
Не удалось построить прямые
Пересечение квантильных прямых
BTCUSD, D1
2 265
95.9
13.2 %
0
9.1 %
EURUSD, D1
2 880
78.1
5.6 %
0
2.9 %
XAUUSD, D1
1 590
84.2
8.6 %
0
5.4 %
SPX500, D1
862
81.0
6.6 %
0
7.4 %

Среднее число итераций составляет от 78 до 96 — на порядок больше тех «нескольких итераций», которых можно было бы ожидать; поэтому предельное число итераций равно 200, а не 50. Тем не менее в 5.6–13.2 % окон предельное число итераций достигается; результаты для этих окон не ошибочны — просто сходимость с заданным допуском не достигнута, а поскольку допуск составляет одну миллионную текущего уровня цены, практическая разница намного меньше одного тика. Механизм защиты от вырожденных систем в SolveWLS ни разу не сработал примерно в 7 600 окнах.

Доля окон с пересечением квантильных прямых заслуживает внимания. В 2.9–9.1 % окон получается канал, квантильные прямые которого на правом краю расположены в неправильном порядке; для BTCUSD это почти один бар из одиннадцати. Такие бары не дают торгового сигнала: по ним не следует торговать или отображать на них канал так, будто он корректен; именно для этого существует Crossed(), и именно поэтому советник пропускает такие бары. Это плата за независимое движение двух границ; у симметричных полос Боллинджера такой проблемы нет.


Ширина канала как признак волатильности

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

Сначала необходимо учесть одну тонкость. Исходная ширина канала измеряется в единицах цены, а за рассматриваемый исторический период цена биткоина менялась на порядок, поэтому при цене около 60 000 абсолютная ширина канала больше, чем при цене около 6 000, независимо от волатильности рынка. Поэтому мы сравниваем с реализованной волатильностью два показателя: исходную абсолютную ширину канала и ширину канала, деленную на текущую цену. Реализованная волатильность — это стандартная оценка волатильности, не зависящая от модели: стандартное отклонение логарифмических доходностей за предшествующий период.

Сравнение проводилось на 2 265 дневных барах с окном канала в 100 баров и глубиной расчета реализованной волатильности в 20 баров:

Сравнение
Коэффициент корреляции Пирсона
Интерпретация
Абсолютная ширина канала и реализованная волатильность
0.010
Связи нет: доминирует тренд уровня цены
Нормированная ширина канала и реализованная волатильность
0.595
Сильная связь: ширина отражает разброс данных
Нормированная ширина канала и будущая реализованная волатильность
0.301
Умеренная связь: есть некоторая опережающая информация

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

То, насколько сильно ослабевает эта корреляция, зависит от инструмента. Там, где уровень цены сравнительно стабилен, исходная ширина сохраняет связь с волатильностью: корреляция составляет 0.450 для EURUSD, 0.530 для XAUUSD и 0.341 для SPX500. Таким образом, нормирование — не косметическое усовершенствование: именно оно отличает работающий признак от бессмысленного на инструментах с наиболее выраженным ценовым трендом, к которым признак волатильности, скорее всего, и будут применять.

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

Нормированная ширина канала и реализованная волатильность изменяются синхронно

Рис. 3. Нормированная ширина канала и реализованная волатильность на дневных данных BTCUSD; оба показателя стандартизированы с помощью z-оценок. Коэффициент корреляции Пирсона — 0.59.

По сравнению с чем?

Коэффициент корреляции 0.6 сам по себе выглядит внушительно, но утверждение «этот показатель отражает волатильность» не является убедительным, пока мы не узнаем, какие результаты дают обычные инструменты при анализе тех же данных. Поэтому для сравнения мы взяли три показателя: ширину полос Боллинджера, средний истинный диапазон (ATR) и ширину обычного регрессионного канала, рассчитанную по остаткам. От одного методологического условия зависит, имеет ли сравнение смысл: все показатели должны рассчитываться с одинаковой глубиной расчета. Если сравнивать наш 100-барный канал с обычным средним истинным диапазоном (ATR) с периодом 20, используя в качестве целевого показателя реализованную волатильность за 20 баров, то корреляция среднего истинного диапазона (ATR) с ней будет близка к 0.9 в силу самого построения: это две оценки одной и той же величины на одном и том же окне. Поэтому все приведенные ниже показатели для сравнения рассчитываются на том же 100-барном окне, что и канал.

Инструмент
Ширина квантильного канала
Ширина полос Боллинджера
Средний истинный диапазон (ATR)
Ширина регрессионного канала
BTCUSD, D1
0.595
0.560
0.404
0.459
EURUSD, D1
0.477
0.474
0.694
0.612
XAUUSD, D1
0.353
0.672
0.530
0.470
SPX500, D1
0.378
0.153
0.509
0.319

Ширина квантильного канала конкурентоспособна, но не превосходит другие показатели. Она показывает лучший результат на дневных данных биткоина — инструмента, с которого началась эта статья; именно такое совпадение и приводит к завышенным выводам в статье, если не проверить другие инструменты. На трёх таймфреймах для каждого из четырёх инструментов (всего двенадцать ячеек) ширина квантильного канала занимает первое место в двух случаях; средний истинный диапазон (ATR) — в семи, ширина полос Боллинджера — в трёх, а ширина регрессионного канала нигде не побеждает.

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

Устойчив ли эффект во времени?

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

Инструмент
Полная история
Первая треть
Вторая треть
Заключительная треть
BTCUSD, D1
0.595
0.624
0.414
0.414
EURUSD, D1
0.477
0.290
0.541
0.537
XAUUSD, D1
0.353
0.078
0.405
0.392
SPX500, D1
0.378
0.099
0.661
0.131

Знак остаётся неизменным, а величина — нет. Для SPX500 значение меняется с 0.099 до 0.661, а затем до 0.131 на трёх последовательных отрезках его истории, а для XAUUSD сначала составляет 0.078, затем стабилизируется около 0.4. Любое решение, построенное на основе этого признака, должно допускать изменение коэффициента в три раза.

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

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


Проверка в тестере стратегий

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

Устройство этого советника важнее его результатов. Советник со стоп-лоссом, целевым уровнем прибыли, правилом трейлинга и фильтром торговых сессий позволяет судить о стоп-лоссе, целевом уровне, правиле трейлинга и фильтре сессий; чтобы изучить канал, нужно убрать всё остальное, оставив канал единственным фактором, от которого может зависеть результат. Поэтому QuantileChannelEA.mq5 намеренно упрощен: только одна позиция одновременно, фиксированный лот, без стоп-лосса и целевого уровня прибыли, решения только по закрытым барам и ни одного параметра, оцененного по данным. В его основе лежат два прямо противоположных правила входа:

//--- the two modes are exact opposites on the same two conditions
   int sig = 0;
   if(px < lower)
      sig = (InpMode == QC_REVERSION ? 1 : -1);
   else
      if(px > upper)
         sig = (InpMode == QC_REVERSION ? -1 : 1);

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

//--- one exit for both modes, so only the entry differs between them
   if(dir != 0)
     {
      if((dir > 0 && px >= mid) || (dir < 0 && px <= mid))
        {
         g_pending = 2;
         Execute();
        }
      return;
     }

Ещё одна строка кода показывает, чем полезна проведённая ранее проверка. Прежде всего советник отбрасывает окна, в которых построенные прямые расположены в неправильном порядке:

//--- a crossed channel is a broken fit, not a signal: sit it out
   if(channel.Crossed())
      return;

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

Во всех приведенных ниже тестах используются одинаковые настройки: бары D1 с 01.01.2015 по 01.08.2026, окно 100 баров, tailProb 0.1, цены закрытия, объем 0.01 лота, депозит 100 000 USD, кредитное плечо 1:100 и модель тестера «OHLC на M1». Оптимизация не проводилась, и ни одна конфигурация не выбиралась по результатам ее тестирования.

Инструмент и режим
Сделки
Чистая прибыль
Профит-фактор
Математическое ожидание выигрыша
Максимальная просадка
BTCUSD — возврат к среднему
59
-1940.66
0.378
-32.89
1.96 %
BTCUSD — пробой
395
283.33
1.102
0.72
0.21 %
EURUSD — возврат к среднему
112
-265.35
0.738
-2.37
0.34 %
EURUSD — пробой
687
-405.08
0.758
-0.59
0.51 %
XAUUSD — возврат к среднему
67
-1803.90
0.513
-26.92
1.87 %
XAUUSD — пробой
334
-2626.10
0.534
-7.86
2.78 %
SPX500 — возврат к среднему
39
-66.91
0.805
-1.72
0.16 %
SPX500 — пробой
212
-117.93
0.771
-0.56
0.19 %

Семь из восьми конфигураций приносят убыток, а результат восьмой нельзя считать убедительным. Торговля на пробой по BTCUSD приносит 283 денежные единицы за 395 сделок; математическое ожидание выигрыша составляет 0.72 денежной единицы на сделку при объеме 0.01 лота, а профит-фактор — 1.10: столь небольшое преимущество может полностью нивелироваться спредами и комиссиями. Еще показательнее то, что эта закономерность не повторяется: если бы торговля на пробой действительно была правильным способом использования этого канала, она не должна была бы приносить убытки на остальных трех инструментах, однако убыточна на всех трех. Ни один из двух подходов не дает убедительного преимущества — это однозначный ответ на вопрос, поставленный в этом разделе. Сами по себе границы канала не служат надежным основанием для входа в рынок.

Ширина как фильтр, а не как сигнал

Поскольку наиболее информативным показателем канала является его ширина, а не положение, в последнем тесте к тому же правилу торговли на пробой добавляется фильтр по нормированной ширине: сделки совершаются только тогда, когда она выше или ниже порога 0.20:

Торговля на пробой по BTCUSD с фильтром по ширине канала
Сделки
Чистая прибыль
Профит-фактор
Математическое ожидание выигрыша
Без фильтра
395
283.33
1.102
0.72
Только широкие каналы
234
-16.65
0.990
-0.07
Только узкие каналы
173
531.10
1.589
3.07

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

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

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


Заключение

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

  • Прямые действительно соответствуют условным квантилям. Охват внутри выборки достигает целевых значений для четырёх классов активов, следует теоретической границе p/n по мере увеличения окна и соответствует вероятности в хвосте распределения при любом её заданном значении.
  • Они носят описательный, а не прогнозный характер. Вне выборки доля следующих баров, оказавшихся за пределами полосы, более чем вдвое превышает номинальную. Это наиболее вероятный способ неправильного использования индикатора.
  • Ширина канала — действительно признак волатильности, но ничем не выдающийся. После нормирования на уровень цены ширина канала положительно связана с реализованной волатильностью во всех проведённых тестах, однако средний истинный диапазон (ATR) с тем же горизонтом чаще всего превосходит её, а её корреляция с реализованной волатильностью зависит от периода.
  • Границы канала не являются источником сигналов входа. Советник с намеренно наивной логикой торговли по этим границам оказался убыточным в семи из восьми конфигураций, причём два правила входа были прямо противоположными, так что не осталось направления торговли, в котором он мог бы оказаться прав.

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

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

Файл
Тип
Описание
QuantileRegression.mqh
Подключаемый файл
Классы CQuantileLine и CQuantileChannel: построение квантильных прямых методом итеративно перевзвешенных наименьших квадратов (IRLS) и канал из трёх прямых с показателями ширины канала (Spread) и асимметрии канала (Skew).
QuantileChannel.mq5
Индикатор
Отображает нижнюю, медианную и верхнюю квантильные прямые на ценовом графике.
QuantileChannelGauge.mq5
Индикатор
Отображает ширину канала и асимметрию канала в отдельном подокне.
QuantileChannelEA.mq5
Советник
Намеренно наивный советник для тестирования: противоположные входы на возврат к среднему и на пробой с общим выходом по медиане и необязательным фильтром рыночного режима по ширине канала.
QC_Validation.mq5
Скрипт
Воспроизводит все таблицы статьи: охват квантилей внутри выборки и вне выборки, сравнение ширины квантильного канала с шириной полос Боллинджера, средним истинным диапазоном (ATR) и шириной регрессионного канала при одинаковом временном горизонте, стабильность на отдельных подпериодах, а также число итераций, неудачных построений и пересечений квантильных прямых при работе алгоритма оценивания.

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

Прикрепленные файлы |
MQL5.zip (17.61 KB)
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Машинное обучение онлайн для фильтрации торговых сигналов в MQL5 (Часть 1): Фильтр, обучающийся на результатах закрытых сделок Машинное обучение онлайн для фильтрации торговых сигналов в MQL5 (Часть 1): Фильтр, обучающийся на результатах закрытых сделок
В этой статье на чистом MQL5 реализован фильтр торговых сигналов на основе логистической регрессии с онлайн-обучением, интегрированный в советник (EA) на основе пересечения экспоненциальных скользящих средних (EMA) с контуром обратной связи по закрытым сделкам. В статье подробно описаны общий класс, признаки, обновление методом стохастического градиентного спуска (SGD), сохранение состояния модели и отображение вероятности без возможности изменения модели. Эксперименты на синтетических данных охватывают оценку разделения при разных начальных значениях генератора случайных чисел, калибровку вероятностей, абляционный анализ признаков, сравнение с базовыми моделями в условиях смены режима и перебор значений гиперпараметров. Вы получите скрипты для воспроизводимых экспериментов и протокол пошаговой проверки со сдвигом временных окон, чтобы проверить фильтр на выбранном вами торговом инструменте.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Как разработать мультивалютного советника с ИИ-помощником (AI Assistant) в MetaTrader 5 Как разработать мультивалютного советника с ИИ-помощником (AI Assistant) в MetaTrader 5
Собрать мультисимвольного советника, да чтобы еще и торговал по разным стратегиям — это задача со звездочкой даже для алготрейдера с многолетним опытом. Но с появлениям полноценного встроенного AI помощника в терминале MetaTrader 5 теперь это посильно разработчику любого уровня. При этом даже не требуется быть специалистом в математике и риск-менеджменте — языковая модель все вам объяснит в чате и напишет полностью за вас код.