Альтернатива среднему арифметическому и стандартному отклонению: Библиотека робастной статистики для индикаторов MQL5
Содержание
- Введение
- Почему классические инструменты уязвимы
- Общая библиотека: медиана, MAD и оценка Тейла — Сена
- Три индикатора для непосредственной замены классических
- Наглядное сравнение — совместное отображение индикаторов
- Подтверждение — точка разрушения
- Граничные случаи и подводные камни
- Заключение
Введение
Практически каждый статистический индикатор на графике MetaTrader строится на основе двух величин: среднего арифметического и стандартного отклонения. Центральная линия полос Боллинджера — скользящая средняя, а их ширина определяется стандартным отклонением. Канал линейной регрессии строится вокруг прямой, подобранной методом наименьших квадратов (МНК), а его ширина определяется стандартным отклонением остатков. Осциллятор z-оценки, или сигма-осциллятор, показывает, на сколько стандартных отклонений последняя цена отклоняется от среднего арифметического. Эти инструменты используются повсеместно: среднее арифметическое и стандартное отклонение легко интерпретировать, их вычисление требует мало ресурсов, а для данных без аномалий они подходят как нельзя лучше.
Проблема в том, что ценовые данные не свободны от аномалий. Достаточно одного ошибочного тика, гэпа после выходных, скачка цены на новостях, срабатывания серии стоп-ордеров или аномальной котировки в условиях низкой ликвидности, чтобы возник выброс, перед которым среднее арифметическое и стандартное отклонение беззащитны. Среднее арифметическое смещается в сторону выброса, стандартное отклонение резко возрастает, и все построенные на их основе полосы, каналы и осцилляторы остаются искаженными, пока выброс не выйдет из расчетного окна. Индикатор, на который вы полагались для описания "нормального" поведения цены, в самый неподходящий момент вместо этого описывает выброс.
В статье мы предлагаем решение на основе робастной статистики — раздела статистики, предназначенного именно для таких ситуаций. Это небольшая библиотека и три использующих ее индикатора для непосредственной замены классических. Вместо среднего арифметического мы используем медиану; вместо стандартного отклонения — медианное абсолютное отклонение (MAD); а вместо наклона, рассчитанного методом наименьших квадратов, — наклон Тейла — Сена, то есть медиану всех попарных наклонов. Каждая из этих оценок отличается от своего классического аналога высокой точкой разрушения — долей данных, которую можно исказить, прежде чем оценка потеряет смысл. Для медианы и MAD эта доля составляет 50%; для среднего арифметического и стандартного отклонения она равна нулю.
Вы получите подключаемый файл RobustStats.mqh с единой реализацией всех расчетов. Он вычисляет как робастные оценки, так и их классические аналоги на одном общем окне, что позволяет сравнивать их на равных условиях. На его основе построены три индикатора: трендовый канал Тейла — Сена, полоса на основе медианы и MAD и осциллятор, нормализованный по MAD. Каждый из них непосредственно заменяет привычный классический инструмент. Четвертый индикатор совместно отображает классические и робастные полосы на свечном графике, позволяя увидеть разницу. Скрипт измеряет точку разрушения на реальных ценовых данных и показывает эту разницу в числах. Цель — воспроизводимая инженерная реализация методов оценки: код должен компилироваться и работать, а результаты — точно соответствовать теории, что должно подтверждаться проверками. Мы не утверждаем, что применение этих инструментов делает торговлю прибыльнее.
Почему классические инструменты уязвимы
Чтобы точно понять суть проблемы, полезно дать ей название. В робастной статистике ключевое свойство оценки — ее точка разрушения: минимальная доля наблюдений в выборке, которую нужно заменить произвольными значениями, чтобы результат оценки можно было сделать произвольным. Этот показатель характеризует, сколько искаженных наблюдений может выдержать оценка.
Точка разрушения среднего арифметического составляет 0%. Возьмите любое окно цен, выберите один бар и устремите его цену к бесконечности — среднее арифметическое также будет неограниченно расти. Достаточно одного искаженного наблюдения из тысячи, чтобы сместить среднее арифметическое сколь угодно далеко. Со стандартным отклонением ситуация еще хуже: при его расчете отклонения возводятся в квадрат, поэтому один выброс не просто меняет его значение, а резко увеличивает его. Оценка наклона методом наименьших квадратов имеет тот же недостаток: метод минимизирует сумму квадратов остатков, поэтому точка, наиболее удаленная от прямой, сильнее всего влияет на подгонку, а одиночный скачок цены может изменить знак наклона.
Медиана показывает совсем другую картину. Чтобы придать медиане окна произвольное значение, нужно исказить половину наблюдений. Медиана — центральное значение отсортированной выборки, поэтому для нее важно количество выбросов, а не то, насколько далеко они находятся. Ее точка разрушения составляет 50% — это максимальное значение, возможное для любой оценки. Медианное абсолютное отклонение обладает той же точкой разрушения — 50%, поскольку само является медианой (абсолютных отклонений от медианы). А у оценки Тейла — Сена точка разрушения составляет примерно 29% — значительно больше нуля у оценки методом наименьших квадратов.
Эти три робастные оценки определяются следующим образом. Медиана окна — центральное значение отсортированного ряда цен закрытия (среднее арифметическое двух центральных значений, если длина окна четная). MAD — это медиана абсолютных отклонений от этой медианы:
MAD = median( |x_i - median(x)| )
У MAD другой масштаб, чем у стандартного отклонения, поэтому мы умножаем его на константу 1,4826. Этот коэффициент выбран так, чтобы для чистых данных с нормальным распределением величина 1,4826 * MAD сходилась к тому же значению, что и стандартное отклонение. Именно поэтому MAD может непосредственно заменить стандартное отклонение: на данных без аномалий робастная и классическая полосы почти полностью совпадают и расходятся лишь при появлении выброса. Наклон Тейла — Сена — это медиана наклонов прямых, проведенных через все пары точек в окне:
наклон = медиана( (y_j - y_i) / (x_j - x_i) ) для всех пар i < j
Поскольку используется медиана попарных наклонов, пары с участием выброса составляют меньшинство и не определяют итоговую оценку. Поэтому результат подгонки по-прежнему определяется основной массой данных. Теперь, когда определения заданы, мы можем один раз реализовать вычисление этих оценок в общей библиотеке.
Общая библиотека: медиана, MAD и оценка Тейла — Сена
Весь набор инструментов основан на одном подключаемом файле RobustStats.mqh, который содержит единственный класс CRobustStats. Все расчеты сосредоточены здесь в единой реализации и не дублируются в индикаторах; каждый индикатор служит лишь оболочкой для визуального представления результатов этого класса. Этот класс выполняет две задачи одновременно: вычисляет робастные оценки (медиану, MAD и наклон Тейла — Сена) и для того же самого окна — их классические аналоги (среднее арифметическое, стандартное отклонение и наклон, рассчитанный методом наименьших квадратов). Вычисление обеих групп оценок здесь, на одних и тех же данных, обеспечивает корректность последующего сравнения: исключается использование разных периодов или разных правил расчета. Файл начинается с блока комментариев, описывающего эти правила работы, за которым следует единственная константа масштабирования:
//+------------------------------------------------------------------+ //| RobustStats.mqh | //| Robust (median-based) statistics with their classical twins. | //| | //| Robust estimators and the classical twins they replace: | //| Median replaces arithmetic Mean | //| MAD (x1.4826) replaces population StdDev | //| Theil-Sen slope replaces ordinary least-squares (OLS) slope | //| MAD-score replaces z-score | //| Both sides are computed over the *same* window so a caller can | //| compare them apples-to-apples on identical data. | //+------------------------------------------------------------------+ #property copyright "Adeolu Kayode" #property strict #define ROBUST_MAD_SCALE 1.4826 // MAD -> sigma consistency factor for Gaussian data
Вместе с классом используется небольшой тип данных — структура, объединяющая все оценки. Благодаря ей ComputeAll() может за один проход по окну вернуть сразу и робастные оценки, и их классические аналоги:
//+------------------------------------------------------------------+ //| Bundle of every estimate from a single window pass. ComputeAll() | //| fills one of these so an indicator that needs both the robust | //| and classical figures (like the comparison panel) sweeps the | //| window a minimal number of times instead of once per accessor. | //+------------------------------------------------------------------+ struct SRobustEstimates { double median; // robust center double mad; // median absolute deviation (raw) double mad_sigma; // ROBUST_MAD_SCALE * mad (sigma-equivalent) double mean; // classical center (twin of median) double stddev; // population standard deviation (twin of mad_sigma) double ts_slope; // Theil-Sen slope (price per bar) double ts_intercept; // Theil-Sen intercept at the newest bar double ols_slope; // least-squares slope (twin of ts_slope) double ols_intercept; // least-squares intercept at the newest bar };
Класс хранит последние N цен закрытия в кольцевом буфере фиксированного размера, поэтому при добавлении нового бара самое старое значение перезаписывается за постоянное время без сдвига элементов массива. Жизненный цикл выглядит следующим образом: сначала вызывается Init(window), затем Update(close) один раз на каждый бар, после этого Ready() для проверки заполнения окна и, наконец, любой метод доступа. В блоке комментариев перед классом описано устройство кольцевого буфера, а далее приведены закрытые поля, хранящие его состояние:
//+------------------------------------------------------------------+ //| CRobustStats -- rolling-window robust statistics. | //| | //| Holds the last N closes in a fixed ring buffer. A new close | //| overwrites the oldest slot in O(1), and Slot(i) maps the logical | //| index "i bars ago" (0 = most recent) to its physical slot with | //| modular arithmetic. There is no array shift, so the push cost is | //| constant regardless of window size. | //+------------------------------------------------------------------+ class CRobustStats { private: double m_close[]; // fixed ring storage, physical slots (see Slot()) int m_window; // number of closes each estimate is computed over int m_cap; // ring capacity = window (closes only, no lagged bar) int m_count; // real bars pushed so far (for warm-up / Ready) int m_head; // physical slot that currently holds the most recent bar //--- reused Theil-Sen work buffer, filled by the slope estimators. It //--- is why TheilSenSlope()/ComputeAll() are non-const (they mutate //--- this member), while the pure read-only accessors stay const. double m_pairs[]; // Theil-Sen pairwise-slope buffer, sized N(N-1)/2 //--- map logical index (i bars ago, 0 = most recent) to a physical ring slot int Slot(const int i) const { int s = m_head - i; if(s < 0) s += m_cap; return(s); }
Вспомогательная функция Slot() определяет всю логику обращения к кольцевому буферу. m_head — индекс элемента массива с данными самого нового бара; при запросе "i баров назад" из m_head вычитается i. Если результат отрицательный, прибавление + m_cap переносит индекс в конец массива. Элементы не перемещаются в памяти: при появлении нового бара изменяется индекс m_head, а самое старое значение в соответствующей ячейке перезаписывается. Именно поэтому Update() имеет сложность O(1) независимо от размера окна.
Одно решение в реализации стоит отметить сразу: оно удивит тех, кто ожидает такого же поведения, как в C++. В языке MQL5 нет ключевого слова mutable. Память под буфер m_pairs выделяется один раз, и он используется повторно при каждом вычислении оценки Тейла — Сена, чтобы не выделять память внутри OnCalculate. Однако методы, заполняющие этот буфер, изменяют его содержимое и потому не могут быть объявлены с квалификатором const. Именно поэтому TheilSenSlope() и ComputeAll() не имеют квалификатора const, тогда как методы доступа, которые только читают данные, сохраняют его. Это мелочь, но именно из-за таких деталей аккуратно перенесенный код на C++ может не скомпилироваться.
Жизненный цикл — Init, Update, Ready. Прежде чем вычислять любую оценку, нужно задать размер окна и передать данные объекту класса. Init() задает размер окна, однократно выделяет память под кольцевой буфер и буфер попарных наклонов (поэтому на каждом баре память выделять не нужно) и сбрасывает счетчики. Update() добавляет одну цену закрытия и изменяет индекс головы кольцевого буфера. Неконечные или неположительные значения отклоняются, чтобы единичная ошибка в потоке данных не испортила содержимое окна. Ready() сообщает, накоплено ли уже достаточно данных для полного окна:
//--- set the estimation window (bars) and allocate the ring plus the //--- pairwise-slope buffer, sized for the full pair set so the Theil- //--- Sen estimator never allocates per call. void Init(const int window) { m_window = (window < 2 ? 2 : window); m_cap = m_window; ArrayResize(m_close, m_cap); ArrayInitialize(m_close, 0.0); ArrayResize(m_pairs, (m_window * (m_window - 1)) / 2); m_count = 0; m_head = -1; // no bar yet; first Update() lands in slot 0 } //--- feed one completed bar's close; call once per bar in chart order. //--- A non-finite or non-positive close is rejected so a single bad //--- tick cannot poison the window. Returns true if it was accepted. bool Update(const double c) { if(!MathIsValidNumber(c) || c <= 0.0) return(false); m_head = (m_head + 1) % m_cap; // advance ring head, overwriting oldest slot m_close[m_head] = c; if(m_count < m_cap) m_count++; return(true); } //--- true once a full window of closes has been seen. bool Ready() const { return(m_count >= m_window); }
Проверка входных данных в Update() — первая линия защиты в робастной библиотеке. Еще до вычисления медианы значение NaN или неположительная цена (которые могут появиться из-за сбоя в потоке данных или поврежденного исторического бара) отклоняются на входе, а не попадают в окно, где могли бы нарушить все последующие сортировки. Эта защитная проверка требует мало ресурсов и полностью исключает недопустимые данные из вычислений.
Базовые компоненты. Вычисление каждой робастной оценки в этом классе сводится к нахождению медианы, поэтому закрытые вспомогательные функции выполняют сортировку и выбирают центральное значение. CopyWindow копирует N цен закрытия из кольцевого буфера в обычный массив в хронологическом порядке (сначала самые старые). Именно такой порядок нужен для задания оси x при вычислении оценок наклона:
//--- copy the N window closes into dst[] in chronological order //--- (dst[0] = oldest bar, dst[n-1] = newest). Used to fill the sort //--- buffer and as the y-values for the slope estimators. void CopyWindow(double &dst[]) const { for(int x = 0; x < m_window; x++) dst[x] = m_close[Slot(m_window - 1 - x)]; // oldest first }
Стоит разобрать выражение для вычисления индекса: Slot(m_window - 1 - x) означает, что при x = 0 мы запрашиваем самый старый бар (m_window - 1 баров назад), а при x = m_window - 1 — самый новый (0 баров назад). Так порядок кольцевого буфера "сначала самые новые" преобразуется в хронологический порядок, используемый в остальных расчетах класса. Поэтому при последующем вычислении любой оценки dst[0] соответствует началу окна, а dst[n-1] — текущему моменту.
SortAscending реализует обычную сортировку вставками. Этот выбор сделан сознательно: сортируемые массивы невелики (само окно или набор попарных наклонов), сортировка выполняется в исходном массиве без дополнительного выделения памяти, а алгоритм настолько прост, что ошибиться практически негде. При таких размерах массивов более сложный алгоритм сортировки лишь усложнил бы реализацию, не дав измеримого выигрыша:
//--- simple in-place ascending sort of a[0..n-1]. Insertion sort is //--- used deliberately: n here is the window (median/MAD) or the pair //--- count, both modest, and it needs no auxiliary storage. void SortAscending(double &a[], const int n) const { for(int i = 1; i < n; i++) { double key = a[i]; int j = i - 1; while(j >= 0 && a[j] > key) { a[j + 1] = a[j]; j--; } a[j + 1] = key; } }
Последние две вспомогательные функции вычисляют медиану по элементам массива. MedianOfSorted работает с уже отсортированным массивом и выбирает центральный элемент (или усредняет два центральных элемента при четной длине). MedianInPlace сначала сортирует массив, а затем вызывает MedianOfSorted. Именно MedianInPlace используется при вычислении оценок для временных массивов, порядок элементов которых можно менять:
//--- median of an already-sorted array a[0..n-1] (average of the two //--- middle elements when n is even). No sorting is done here. double MedianOfSorted(const double &a[], const int n) const { if(n <= 0) return(0.0); int mid = n / 2; if((n & 1) == 1) return(a[mid]); return(0.5 * (a[mid - 1] + a[mid])); } //--- sort a[0..n-1] ascending in place, then return its median. Used //--- for the pairwise-slope set and the residual sets where the input //--- is a scratch array we are free to reorder. double MedianInPlace(double &a[], const int n) const { if(n <= 0) return(0.0); SortAscending(a, n); return(MedianOfSorted(a, n)); }
Такое разделение вычисления медианы на две операции позволяет реализовать описанный далее однопроходный ComputeAll(): он может один раз отсортировать окно, а затем вызвать MedianOfSorted без затрат на повторную сортировку. Теперь метод доступа к медиане выполняет всю основную работу в трех строчках: копирует окно, сортирует его и находит центральное значение:
//--- Median: rolling median of the window. Copies the window into the //--- scratch buffer, sorts it, and picks the middle element(s). double Median() const { if(!Ready()) return(0.0); double tmp[]; ArrayResize(tmp, m_window); CopyWindow(tmp); return(MedianInPlace(tmp, m_window)); }
MAD вычисляется непосредственно по определению: это медиана абсолютных отклонений от медианы, поэтому медиану нужно вычислить дважды — сначала для значений окна, затем для их абсолютных отклонений. Обратите внимание на небольшую оптимизацию: после первого вызова MedianInPlace массив tmp отсортирован, но содержит тот же набор значений. Поэтому каждый его элемент можно заменить абсолютным отклонением этого значения от медианы, а затем снова вычислить медиану:
//--- MAD: median of the absolute deviations |x_i - median|. Two median //--- passes over the window -- one for the center, one for the folded //--- deviations. Returned raw (unscaled); see MadSigma() for sigma. double MAD() const { if(!Ready()) return(0.0); double tmp[]; ArrayResize(tmp, m_window); CopyWindow(tmp); double med = MedianInPlace(tmp, m_window); for(int i = 0; i < m_window; i++) tmp[i] = MathAbs(tmp[i] - med); // fold to |x - median| (same multiset, now sorted) return(MedianInPlace(tmp, m_window)); }
Масштабирование с коэффициентом 1,4826 и вычисление робастной z-оценки реализуются на основе MAD — каждое в одну строчку. MadSigma() возвращает эквивалент стандартного отклонения, а MadScore() — робастную z-оценку, проверяя MAD на равенство нулю (когда цены во всем окне одинаковы), чтобы исключить деление на ноль:
//--- MadSigma: MAD scaled to a standard-deviation equivalent. For //--- Gaussian data 1.4826*MAD converges to the population sigma, so //--- this is the drop-in replacement for StdDev in a band width. double MadSigma() const { return(ROBUST_MAD_SCALE * MAD()); } //--- MadScore: robust z-score of an arbitrary value against the window, //--- (value - median) / (1.4826*MAD). Guards a zero MAD (a flat window) //--- by returning 0 rather than dividing by zero. double MadScore(const double value) const { double s = MadSigma(); if(s <= 0.0) return(0.0); return((value - Median()) / s); }
Наклон Тейла—Сена. Это ядро библиотеки и компонент, которому нет аналога среди встроенных средств MetaTrader. Эта оценка представляет собой точную медиану всех попарных наклонов в пределах окна. Для окна из N точек число пар равно N(N-1)/2, поэтому вычислительная сложность составляет O(n^2). Такие вычислительные затраты приняты сознательно и прямо оговорены в коде: здесь используется точная оценка, а не приближение. Разработчик намеренно выбрал точное вычисление на окне умеренного размера вместо приближенного вычисления на большом окне. Набор попарных наклонов записывается в буфер m_pairs, память под который выделена заранее, после чего медиана вычисляется в том же буфере. Обратите внимание на заданное в комментарии направление оси x: ее значения изменяются от 0 на самом старом баре до N-1 на самом новом, поэтому положительный наклон означает рост цены:
//--- TheilSenSlope: the exact median of all n(n-1)/2 pairwise slopes //--- (y_j - y_i) / (x_j - x_i) over the window, x = 0..n-1 oldest to //--- newest. This is O(n^2) by design -- it is the exact estimator, //--- not an approximation. The pair set is written into m_pairs (sized //--- once in Init) and its median is taken in place. double TheilSenSlope() { if(!Ready()) return(0.0); double y[]; ArrayResize(y, m_window); CopyWindow(y); // y[x], x = 0 oldest .. n-1 newest int k = 0; for(int i = 0; i < m_window - 1; i++) { for(int j = i + 1; j < m_window; j++) { m_pairs[k] = (y[j] - y[i]) / (double)(j - i); // x-gap is (j - i) k++; } } return(MedianInPlace(m_pairs, k)); }
Чтобы построить канал, нужен не только наклон прямой, но и ее уровень. TheilSenIntercept() принимает наклон и возвращает робастный свободный член — медиану остатков y_i - slope * x_i. Поскольку канал нужно построить на текущем баре, сначала вычисляется свободный член при x = 0 (самый старый бар), затем к нему прибавляется slope * (n - 1). В результате возвращается уровень прямой на самом новом баре:
//--- TheilSenIntercept: given a Theil-Sen slope, the robust intercept //--- is the median of the residuals y_i - slope*x_i. Reported AT THE //--- NEWEST BAR (x = n-1) so a channel center can be drawn directly: //--- b_newest = median(y_i - slope*x_i) + slope*(n-1). double TheilSenIntercept(const double slope) const { if(!Ready()) return(0.0); double y[]; ArrayResize(y, m_window); CopyWindow(y); double r[]; ArrayResize(r, m_window); for(int x = 0; x < m_window; x++) r[x] = y[x] - slope * (double)x; double b0 = MedianInPlace(r, m_window); // intercept at x = 0 (oldest) return(b0 + slope * (double)(m_window - 1)); // shift to newest bar }
Использование медианы остатков (вместо проведения прямой через среднее арифметическое) делает свободный член таким же робастным, как и наклон: скачок цены в окне дает один большой остаток, который медиана игнорирует. Поэтому при появлении проблемного бара канал не меняет угол (робастный наклон) и не смещается по вертикали (робастный свободный член).
Классические аналоги. Библиотека предназначена для сравнения, поэтому для каждой робастной оценки по тому же самому окну вычисляется аналог на основе метода наименьших квадратов. Это те инструменты, которые мы заменяем; они реализованы в самой библиотеке, чтобы сравнение выполнялось на одних и тех же данных без внешних хэндлов индикаторов. Среднее арифметическое и стандартное отклонение генеральной совокупности вычисляются непосредственно. Обратите внимание: в StdDev используется делитель n, как и во встроенных iStdDev и iBands:
//--- Mean: arithmetic mean of the window (classical twin of Median). double Mean() const { if(!Ready()) return(0.0); double sum = 0.0; for(int i = 0; i < m_window; i++) sum += m_close[Slot(i)]; return(sum / m_window); } //--- StdDev: population standard deviation of the window (classical //--- twin of MadSigma). Uses the population divisor n so it lines up //--- with the built-in iStdDev/iBands convention. double StdDev() const { if(!Ready()) return(0.0); double mean = Mean(); double ss = 0.0; for(int i = 0; i < m_window; i++) { double d = m_close[Slot(i)] - mean; ss += d * d; } double var = ss / m_window; return(var > 0.0 ? MathSqrt(var) : 0.0); }
Именно при накоплении суммы ss проявляется уязвимость стандартного отклонения: каждое отклонение возводится в квадрат, поэтому один бар с ценой, далекой от среднего арифметического, добавляет к сумме квадрат этого отклонения. Именно поэтому в разделе "Подтверждение — точка разрушения" один скачок цены увеличивает StdDev на 145%, тогда как основанный на медиане MadSigma почти не меняется. Затем классический ZScore делит отклонение значения от среднего арифметического на это стандартное отклонение, проверяя делитель на равенство нулю так же, как его робастный аналог:
//--- ZScore: classical z-score (value - mean) / stddev (twin of //--- MadScore). Guards a zero StdDev by returning 0. double ZScore(const double value) const { double s = StdDev(); if(s <= 0.0) return(0.0); return((value - Mean()) / s); }
Наклон, рассчитанный методом наименьших квадратов, — аналог наклона Тейла — Сена. Он вычисляется по явной формуле на той же сетке целочисленных значений x. Поскольку x принимает значения фиксированной сетки 0..n-1, его среднее арифметическое постоянно и равно (n-1)/2, а знаменатель Sxx также фиксирован. Поэтому наклон — это просто отношение суммы попарных произведений отклонений Sxy к Sxx:
//--- OlsSlope: ordinary least-squares slope over the same x = 0..n-1 //--- grid (classical twin of TheilSenSlope). Closed form using the //--- centered sums; x is a fixed integer grid so Sxx is a constant. double OlsSlope() const { if(!Ready()) return(0.0); double y[]; ArrayResize(y, m_window); CopyWindow(y); double n = (double)m_window; double xbar = (n - 1.0) / 2.0; // mean of 0..n-1 double ybar = 0.0; for(int x = 0; x < m_window; x++) ybar += y[x]; ybar /= n; double sxy = 0.0, sxx = 0.0; for(int x = 0; x < m_window; x++) { double dx = (double)x - xbar; sxy += dx * (y[x] - ybar); sxx += dx * dx; } return(sxx > 0.0 ? sxy / sxx : 0.0); }
Здесь уязвимость связана с sxy: вклад каждой точки в эту сумму взвешивается по ее отклонению dx от центра окна. Поэтому скачок цены вблизи любого из концов окна может сильно изменить наклон и даже его знак. Именно это покажет таблица результатов на примере одного искаженного бара. OlsIntercept() работает аналогично TheilSenIntercept(): вычисляет свободный член при x = 0, а затем пересчитывает его в уровень прямой на самом новом баре. Этот короткий метод приведен в прилагаемом файле.
Робастная ширина канала. Для канала Тейла — Сена нужно задать полуширину. Чтобы канал оставался робастным на всех этапах вычисления, оценка его ширины тоже должна быть устойчивой к выбросам. ResidualMadSigma() вычисляет эту величину: для подобранной прямой (по ее наклону и уровню на самом новом баре) метод находит MAD остатков относительно нее и умножает результат на 1,4826. Классический канал использовал бы здесь стандартное отклонение остатков и резко расширился бы из-за одного скачка цены. Здесь используется MAD остатков, поэтому такого расширения не происходит:
//--- ResidualMadSigma: robust dispersion of the window about a fitted //--- line, as 1.4826 * median(|y_x - line(x)|). 'intercept_newest' is //--- the intercept at the newest bar (x = n-1), the convention the //--- intercept accessors return, so line(x) = intercept_newest - //--- slope*((n-1) - x). This is the fully robust channel half-width //--- unit for RobustTrend: multiply by the user's k to get the band. double ResidualMadSigma(const double slope, const double intercept_newest) const { if(!Ready()) return(0.0); double y[]; ArrayResize(y, m_window); CopyWindow(y); // y[x], x = 0 oldest .. n-1 newest double r[]; ArrayResize(r, m_window); int last = m_window - 1; for(int x = 0; x < m_window; x++) { double fit = intercept_newest - slope * (double)(last - x); r[x] = MathAbs(y[x] - fit); } return(ROBUST_MAD_SCALE * MedianInPlace(r, m_window)); }
Все оценки за один проход — ComputeAll. Каждый из приведенных выше методов доступа отдельно копирует и сортирует окно. Это вполне приемлемо, когда индикатору нужны один или два таких метода. Но индикатору сравнения нужны все эти значения сразу, а вызов шести методов доступа привел бы к шести проходам по окну и шести сортировкам. Именно для этого предназначен ComputeAll(). Метод один раз копирует окно и сортирует его, чтобы совместно вычислить медиану, MAD, среднее арифметическое и стандартное отклонение, а затем по общей копии вычисляет оба наклона и оба свободных члена. Именно этот метод вызывает индикатор сравнения. Здесь проявляется преимущество единой реализации расчетов: обе группы оценок вычисляются за один проход:
//--- ComputeAll: fill every estimate for the current window. The //--- window is copied and sorted ONCE to derive median, MAD, mean and //--- stddev; the two slopes and their intercepts are then computed //--- from a single shared copy of the (unsorted) window. Returns false //--- until Ready(). bool ComputeAll(SRobustEstimates &out) { out.median = 0.0; out.mad = 0.0; out.mad_sigma = 0.0; out.mean = 0.0; out.stddev = 0.0; out.ts_slope = 0.0; out.ts_intercept = 0.0; out.ols_slope = 0.0; out.ols_intercept = 0.0; if(!Ready()) return(false); double y[]; ArrayResize(y, m_window); CopyWindow(y); // chronological: y[0] oldest .. y[n-1] newest //--- sorted copy for median / MAD, and mean / stddev in the same sweep double s[]; ArrayResize(s, m_window); double sum = 0.0; for(int x = 0; x < m_window; x++) { s[x] = y[x]; sum += y[x]; } SortAscending(s, m_window); double med = MedianOfSorted(s, m_window); double mean = sum / m_window; double ss = 0.0; for(int x = 0; x < m_window; x++) { double d = y[x] - mean; ss += d * d; s[x] = MathAbs(y[x] - med); // fold deviations for MAD (reuse s) } SortAscending(s, m_window); double mad = MedianOfSorted(s, m_window); double var = ss / m_window; out.median = med; out.mad = mad; out.mad_sigma = ROBUST_MAD_SCALE * mad; out.mean = mean; out.stddev = (var > 0.0 ? MathSqrt(var) : 0.0); //--- Theil-Sen slope: median of the pairwise-slope set into m_pairs int k = 0; for(int i = 0; i < m_window - 1; i++) for(int j = i + 1; j < m_window; j++) { m_pairs[k] = (y[j] - y[i]) / (double)(j - i); k++; } double ts_slope = MedianInPlace(m_pairs, k); //--- OLS slope over the same integer x-grid (closed form) double n = (double)m_window; double xbar = (n - 1.0) / 2.0; double sxy = 0.0, sxx = 0.0; for(int x = 0; x < m_window; x++) { double dx = (double)x - xbar; sxy += dx * (y[x] - mean); sxx += dx * dx; } double ols_slope = (sxx > 0.0 ? sxy / sxx : 0.0); //--- Theil-Sen intercept: median residual, shifted to the newest bar double r[]; ArrayResize(r, m_window); for(int x = 0; x < m_window; x++) r[x] = y[x] - ts_slope * (double)x; double ts_b0 = MedianInPlace(r, m_window); //--- OLS intercept at the newest bar double ols_b0 = mean - ols_slope * xbar; out.ts_slope = ts_slope; out.ts_intercept = ts_b0 + ts_slope * (double)(m_window - 1); out.ols_slope = ols_slope; out.ols_intercept = ols_b0 + ols_slope * (double)(m_window - 1); return(true); }
Во втором цикле рабочий массив используется повторно: за один проход по окну накапливается сумма квадратов отклонений для стандартного отклонения, а элементы массива s заменяются абсолютными отклонениями от медианы. После еще одной сортировки можно вычислить MAD. Медиана, MAD, среднее арифметическое и стандартное отклонение вычисляются за две сортировки и два линейных прохода, а оба наклона — по одной общей копии окна. Именно поэтому индикатор сравнения может строить четыре линии на каждом баре без четырехкратного увеличения объема вычислений.
Три индикатора для непосредственной замены классических
Благодаря готовой библиотеке каждый индикатор служит лишь оболочкой: настраивает свои буферы, передает цены закрытия экземпляру CRobustStats и получает вычисленную оценку. Все три индикатора используют одну схему частичного пересчета на основе prev_calculated: на каждом тике пересчитывается только конечный участок данных. Кольцевой буфер заново заполняется ценами закрытия баров, предшествующих первому пересчитываемому, поэтому уже для первого записываемого результата доступно полное окно. Для повторного заполнения используется еще один метод библиотеки, который пока не упоминался, — Reset(). Он сбрасывает счетчики и индекс головы, но сохраняет выделенную память. Поэтому перед каждым пересчетом конечного участка данных состояние объекта можно сбросить и заново передать ему данные без дополнительного выделения памяти под массивы. Этот сброс требует мало ресурсов и делает пересчет конечного участка данных не только быстрым, но и корректным: окно для первого перезаписываемого результата точно восстанавливается, а не берется из устаревшего состояния. Это стандартная практика корректной реализации индикаторов, поэтому мы разбираем ее здесь один раз и не повторяем объяснение для каждого файла.
RobustBands — медиана плюс-минус MAD. Это прямая замена полосам Боллинджера. Центральная линия — скользящая медиана, а полуширина равна произведению k и значения, возвращаемого MadSigma(). Поскольку на данных без загрязнения 1,4826 * MAD оценивает ту же сигму, что и стандартное отклонение, при спокойном движении цены эта полоса почти неотличима от полосы Боллинджера с тем же периодом. Разница появляется только при попадании выброса в окно. Вот основная часть цикла вычислений, в которой значения, полученные из библиотеки, записываются в три буфера:
g_robust.Update(close[i]); if(!g_robust.Ready()) { UpperBuffer[i] = EMPTY_VALUE; MiddleBuffer[i] = EMPTY_VALUE; LowerBuffer[i] = EMPTY_VALUE; continue; } //--- robust center and half-width for the window ending at i double center = g_robust.Median(); double offset = InpMultiplier * g_robust.MadSigma(); MiddleBuffer[i] = center; UpperBuffer[i] = center + offset; LowerBuffer[i] = center - offset;
Условие g_robust.Ready() ? ... : EMPTY_VALUE обеспечивает корректную обработку начального накопления данных: пока окно не заполнено, осмысленной медианы нет. Поэтому мы записываем EMPTY_VALUE, и терминал не рисует линию, которая могла бы ввести в заблуждение.

Рис. 1. Индикатор RobustBands на ценовом графике: центральная линия — скользящая медиана, полуширина — произведение k и 1,4826 * MAD. При спокойном движении цены он почти совпадает с обычной полосой Боллинджера и расходится с ней лишь при попадании выброса в окно.
RobustTrend — канал Тейла — Сена. Он заменяет канал линейной регрессии. Центральная линия строится по значению прямой Тейла — Сена на текущем баре. Полуширина канала также робастна: она определяется по MAD остатков относительно этой прямой с помощью небольшой вспомогательной функции библиотеки ResidualMadSigma(). Важно сделать робастной и оценку ширины. Классический регрессионный канал расширяется, как только в расчетное окно попадает скачок цены: его ширина определяется стандартным отклонением остатков. Канал Тейла — Сена сохраняет стабильную ширину, поскольку использует MAD остатков. В основном цикле получаются наклон, свободный член и робастная полуширина:
//--- Theil-Sen fit for the window ending at i, evaluated at the //--- newest bar, plus the robust residual half-width double slope = g_robust.TheilSenSlope(); double center = g_robust.TheilSenIntercept(slope); double offset = InpMultiplier * g_robust.ResidualMadSigma(slope, center); CenterBuffer[i] = center; UpperBuffer[i] = center + offset; LowerBuffer[i] = center - offset;

Рис. 2. Канал RobustTrend — центральная линия Тейла — Сена и полуширина на основе MAD остатков. В отличие от канала, построенного методом наименьших квадратов, здесь одиночный скачок цены в окне не меняет наклон центральной линии и не расширяет полосу.
RobustOscillator — MAD-оценка. Он заменяет осциллятор z-оценки, или сигма-осциллятор. Он отображает в отдельном подокне робастную z-оценку последней цены закрытия относительно значений в расчетном окне: (цена закрытия - медиана) / (1,4826 * MAD). Вспомогательные уровни +2, -2, +3 и -3 благодаря калибровке с коэффициентом 1,4826 интерпретируются как границы сигма-полос. Преимущество перед классической z-оценкой неочевидно, но важно: в осцилляторе z-оценки один скачок цены увеличивает и среднее арифметическое, и стандартное отклонение. Это смещает нулевой уровень и сжимает показания всех остальных баров в окне. Базовый уровень MAD-оценки остается стабильным, поэтому скачок цены дает большое показание на соответствующем баре, не искажая показания соседних баров. Весь расчет для каждого бара сводится к одному вызову метода библиотеки:
//--- score this bar's close against the window it just completed
ScoreBuffer[i] = g_robust.MadScore(close[i]); 
Рис. 3. Подокно RobustOscillator — MAD-оценка последней цены закрытия. Вспомогательные линии на уровнях +2, -2, +3 и -3 приблизительно соответствуют границам сигма-полос. Скачок цены дает отдельное большое показание, не смещая базовый уровень, относительно которого оцениваются соседние бары.
Наглядное сравнение — совместное отображение индикаторов
Каждый из трех индикаторов заменяет один классический инструмент. Но чтобы увидеть, что дает эта замена, нужно одновременно отобразить оба инструмента на одной шкале. Индикатор RobustComparison отображает на ценовом графике две пары полос, рассчитанные по одному общему окну. Классические полосы — среднее арифметическое плюс-минус произведение k и StdDev — показаны тонкими серыми линиями; робастные — медиана плюс-минус произведение k и 1,4826 * MAD — толстыми цветными линиями. Важно, что оба результата получаются одним вызовом ComputeAll() по одному и тому же окну, поэтому любое расхождение обусловлено исключительно способом оценки, а не другим периодом или правилом выбора цены. Это снимает очевидное возражение: полосы могли рассчитываться в разных условиях.
if(!g_robust.ComputeAll(e)) { ClearBar(i); continue; } //--- classical band: mean +/- k*StdDev double cls_off = InpMultiplier * e.stddev; ClsMidBuffer[i] = e.mean; ClsUpBuffer[i] = e.mean + cls_off; ClsLoBuffer[i] = e.mean - cls_off; //--- robust band: median +/- k*1.4826*MAD double rob_off = InpMultiplier * e.mad_sigma; RobMidBuffer[i] = e.median; RobUpBuffer[i] = e.median + rob_off; RobLoBuffer[i] = e.median - rob_off;
На участке графика без выбросов обе пары полос почти совпадают. Это тоже важно: сходство робастной полосы с классической не случайно — она откалибрована так, чтобы совпадать с ней на данных без загрязнения. Искать различия следует на участке со скачком цены или гэпом. На таком участке серая классическая полоса резко сдвигается и продолжает смещаться: среднее арифметическое тянется к выбросу, а ширина увеличивается под его влиянием. Цветная робастная полоса при этом сохраняет положение и ширину. Вот и вся суть статьи на одном скриншоте.

Рис. 4. Совместное отображение индикаторов на участке со скачком цены. Когда выброс попадает в расчетное окно, серая классическая полоса (среднее арифметическое плюс/минус StdDev) расширяется и смещается, а цветная робастная полоса (медиана плюс/минус 1,4826 * MAD) почти не меняет положения. Обе вычисляются по одному и тому же окну.
Подтверждение — точка разрушения
Скриншот показывает эффект, а числа его доказывают. Прилагаемый скрипт RobustContaminationDemo измеряет точку разрушения непосредственно на реальных ценовых данных. Он получает с текущего графика цены закрытия последних N завершенных баров, затем последовательно заменяет значения 0, 1, 2, ... баров выбросом (величина выброса задается относительно ценового диапазона окна, поэтому тест имеет смысл на любом инструменте) и на каждом шаге пересчитывает все оценки. Если читать полученную таблицу сверху вниз, видно, что робастная оценка остается неизменной, а классическая смещается с каждым добавленным скачком цены. Когда доля искаженных данных приближается к половине, робастная оценка тоже начинает смещаться — так наглядно проявляется точка разрушения 50%.
Перебор намеренно ограничен значением N/2: при искажении более 50% данных даже робастные оценки теряют устойчивость — именно это здесь и демонстрируется. Измерение выполняет одна вспомогательная функция: она передает данные новому экземпляру класса и вызывает ComputeAll():
//+------------------------------------------------------------------+ //| Feed an array of closes (chronological: [0] oldest) into a fresh | //| library instance sized to the array length, and fill a full set | //| of estimates from it. | //+------------------------------------------------------------------+ bool Measure(const double &closes[], SRobustEstimates &e) { int n = ArraySize(closes); CRobustStats rs; rs.Init(n); for(int i = 0; i < n; i++) rs.Update(closes[i]); return(rs.ComputeAll(e)); }
При запуске на EURUSD M30 с окном в 40 баров и скачком цены величиной в три ценовых диапазона окна скрипт выводит приведенную ниже таблицу. Эта таблица — главное подтверждение выводов статьи, поэтому ее стоит внимательно изучить. Прежде чем перейти к числам, учтем одну оговорку: скрипт считывает исторические данные, доступные в терминале на момент запуска. Поэтому при самостоятельном запуске вы получите другие значения; на другом инструменте, периоде или в другой день различия могут быть значительными. Это не имеет значения. Воспроизводятся не отдельные числа, а закономерность изменения значений в каждом столбце: робастная оценка остается стабильной, а классическая смещается.
| Искажено баров | медиана | среднее значение | MAD-сигма | StdDev | Наклон Тейла — Сена | Наклон МНК |
|---|---|---|---|---|---|---|
| 0 (0%) | 1,137980 | 1,138083 | 0,000556 | 0,000532 | 0,000030 | 0,000026 |
| 1 (2,5%) | 1,138015 | 1,138275 | 0,000563 | 0,001304 | +0,000030 | -0,000002 |
| 3 (7,5%) | 1,138130 | 1,138661 | 0,000608 | 0,001954 | +0,000022 | -0,000054 |
| 6 (15%) | 1,138225 | 1,139239 | 0,000689 | 0,002588 | +0,000003 | -0,000121 |
| 10 (25%) | 1,138395 | 1,140010 | 0,000778 | 0,003104 | -0,000052 | -0,000191 |
| 15 (37,5%) | 1,138715 | 1,140974 | 0,001208 | 0,003424 | -0,000203 | -0,000245 |
| 18 (45%) | 1,138995 | 1,141552 | 0,002039 | 0,003554 | -0,000217 | -0,000260 |
| 20 (50%) | 1,142280 | 1,141937 | 0,005174 | 0,003610 | -0,000217 | -0,000263 |
Таблица 1. Проверка точки разрушения на EURUSD M30: окно — 40 баров, величина скачка цены — три ценовых диапазона окна. Отдельные строки полного перебора с числом искаженных баров от 0 до 20. Значения в столбцах робастных оценок (медиана, MAD-сигма, наклон Тейла — Сена) остаются стабильными, а в столбцах классических оценок (среднее арифметическое, StdDev, наклон МНК) смещаются уже с первого скачка цены.
Вся аргументация опирается на три наблюдения по этой таблице. Первое и самое наглядное наблюдение — изменение наклона МНК при одном скачке цены. В строке 1 один искаженный бар из сорока меняет наклон МНК с +0,000026 на -0,000002 — знак меняется на противоположный. Из-за одного ошибочного тика классическая оценка показала нисходящий тренд вместо восходящего. Наклон Тейла — Сена в той же строке остается равным +0,000030 и становится отрицательным лишь при искажении примерно 18% баров в окне. Одного выброса достаточно, чтобы классическая оценка показала противоположное направление тренда; робастная оценка на него не реагирует.
Второе наблюдение — сравнение StdDev и MAD-сигмы. Тот же одиночный скачок цены увеличивает стандартное отклонение с 0,000532 до 0,001304 — примерно на 145%. Робастная MAD-сигма при этом меняется с 0,000556 до 0,000563 — чуть более чем на 1%. Полоса, ширина которой определяется по StdDev, резко расширилась бы из-за одного ошибочного тика; полоса на основе MAD-сигмы его бы не заметила. Именно это наглядно показывает индикатор сравнения.
Третье и самое изящное наблюдение — проявление точки разрушения в данных. Проследите значения в столбце медианы сверху вниз: на протяжении 18 строк она почти не меняется, смещаясь лишь в пределах ценового диапазона окна. Затем в последней строке, когда искажены 20 баров из 40 — ровно 50%, — она резко возрастает до 1,142280. MAD-сигма ведет себя аналогично: остается вблизи значения для незагрязненных данных, а при искажении 50% данных резко возрастает 0,005174. Это не ошибка и не совпадение: точка разрушения медианы на уровне 50%, ее теоретический предел, проявляется именно там, где и предсказывает математика. Оценка сохраняет устойчивость лишь до тех пор, пока доля искаженных данных не достигнет половины. Среднее арифметическое, напротив, последовательно смещается в каждой строке начиная с первого скачка цены, поскольку его точка разрушения равна нулю.
Стоит подробнее разобрать последнюю строку: внимательный читатель заметит, что при искажении 50% данных MAD-сигма (0,005174) не просто изменилась, а превысила классическое StdDev (0,003610), хотя во всех предыдущих строках уверенно превосходила его по устойчивости. Именно так и должно быть — здесь важно честно обозначить границы применимости метода. Точка разрушения гарантирует ограниченность робастной оценки при искажении менее половины данных, но ничего не гарантирует ровно на этой границе: там набор отклонений от медианы сам наполовину состоит из выбросов, и MAD резко возрастает. Робастная оценка не может бесконечно сохранять приемлемый результат по мере искажения данных — она просто теряет устойчивость последней. К моменту, когда доля искаженных данных достигает этой границы, все преимущества оценки исчерпаны; весь выигрыш приходится на 18 предшествующих строк.
Граничные случаи и подводные камни
Доверие к библиотеке робастной статистики зависит от того, насколько надежно она работает в сложных граничных случаях. Несколько таких случаев стоит оговорить явно: они важны для корректного использования библиотеки, а об этих деталях не расскажет ни один справочник.
Робастность не означает более высокую доходность. Это самое важное предостережение. Все вышесказанное касается статистической робастности оценок — их устойчивости к загрязнению данных. Приведенные результаты доказывают именно это и ничего больше. Из этого не следует, что стратегия, построенная на робастных индикаторах, более прибыльна, чем стратегия, построенная на классических индикаторах. На данных без загрязнения оба варианта по замыслу почти идентичны. Робастный вариант оправдывает себя именно при наличии выбросов, если вы предпочитаете, чтобы индикатор игнорировал их, а не реагировал на них. Нужно ли такое поведение, зависит исключительно от стратегии. Не следует воспринимать результаты проверки точки разрушения как заявление об эффективности торговли.
Вычисление оценки Тейла — Сена требует ресурсов; используйте окно умеренного размера. Вычисление попарных наклонов имеет сложность O(n^2): для окна из 40 баров за один расчет вычисляются 780 наклонов, а для окна из 80 баров — 3 160. Для индикатора с расчетом на каждом баре такая скорость вполне приемлема, но объем вычислений растет квадратично, поэтому использовать окно в несколько сотен баров для оценки Тейла — Сена не стоит. Библиотека один раз выделяет память под буфер попарных наклонов Init(), чтобы не выделять ее при каждом вызове, но число арифметических операций от этого не меняется. Если нужно вычислять робастный наклон на большом окне, усложнение реализации ради приближенного алгоритма Тейла — Сена становится оправданным. Для окон, обычно используемых в индикаторах, точный вариант со сложностью O(n^2) — самое простое корректное решение.
Предел в 50% — это именно предел, а не гарантия точности при 49%. Точка разрушения означает, что при искажении менее 50% данных оценка остается ограниченной, но не обязательно точной. В таблице 1 видно, что медиана плавно смещается задолго до резкого срыва на отметке 50%, а оценка наклона Тейла — Сена постепенно ухудшается уже примерно с 15%. Робастные оценки обеспечивают устойчивость, когда выбросы составляют меньшинство, но не гарантируют верного результата, если треть данных в окне искажена. Если половина баров искажена, ни одна оценка не поможет, а медиана просто потеряет устойчивость последней.
В окнах четной длины усредняются два центральных значения. Медиана окна четной длины равна среднему арифметическому двух центральных порядковых статистик. В библиотеке используется это стандартное определение. При этом медиана может не совпадать ни с одной фактической ценой в окне, а ее значение немного зависит от четности длины окна; такая зависимость безвредна. Об этом стоит помнить, если при сравнении медианы с исходными ценами закрытия окажется, что она находится между двумя из них.
Совпадающие значения и окна с постоянной ценой. Если все цены в окне одинаковы, MAD равно нулю, что привело бы к делению на ноль при расчете робастной z-оценки. В MadScore() предусмотрена явная проверка: если значение MadSigma не положительно, возвращается ноль. Поэтому при неизменной цене результат оценки равен нулю, а не значению "не число". Такая же проверка предусмотрена в классическом методе ZScore() на случай нулевого стандартного отклонения.
Входной фильтр отбрасывает некорректные бары, но не исправляет их. Update() не сохраняет неконечную или неположительную цену закрытия, а отклоняет ее. Для ценовых рядов, на которые рассчитана библиотека, это правильное поведение по умолчанию: оно не допускает NaN в последующие сортировки. Однако отклонение значения не означает его замены: отклоненный бар просто не попадает в окно. Поэтому в кольцевом буфере на одну фактическую цену закрытия меньше, чем можно предположить по индексу бара, а Ready() возвращает false на один бар дольше. При работе с обычными данными валютного и фондового рынков этот защитный механизм никогда не срабатывает. Нужно учитывать инструменты, для которых допустимы нулевые или отрицательные котировки, ряды спредов, а также ряды разностей или данных с вычтенным базовым уровнем. В них неположительные значения — реальные данные, а не результат искажения. В таком случае условие c <= 0.0 во входном фильтре не подходит: его следует убрать, оставив только проверку конечности значения.
Заключение
Мы поставили цель заменить две величины, лежащие в основе большинства статистических индикаторов, — среднее арифметическое и стандартное отклонение — оценками, которые не теряют устойчивость при аномальном поведении цены. В результате получился компактный набор инструментов, основанный на одной идее: медиана и вычисляемые на ее основе величины (MAD, наклон Тейла — Сена) имеют высокую точку разрушения, поэтому небольшая доля ошибочных тиков не может существенно их сместить. Среднее арифметическое, стандартное отклонение и наклон МНК, напротив, имеют нулевую точку разрушения, и один выброс может сместить их сколь угодно далеко. В статье представлены следующие компоненты:
- RobustStats.mqh — общая библиотека. Один класс CRobustStats вычисляет робастные оценки (медиану, MAD, MadSigma, MadScore, наклон и свободный член Тейла — Сена, MAD остатков) и их классические аналоги (среднее арифметическое, StdDev, z-оценку, наклон и свободный член МНК) по одному окну, данные которого хранятся в кольцевом буфере. Также предусмотрен однопроходный метод ComputeAll().
- Три индикатора для непосредственной замены классических. RobustBands (медиана плюс-минус 1,4826 * MAD, заменяет полосы Боллинджера), RobustTrend (канал Тейла — Сена с шириной на основе MAD остатков, заменяет канал линейной регрессии) и RobustOscillator (MAD-оценка, заменяет осциллятор z-оценки). Каждый индикатор служит лишь оболочкой библиотеки и корректно обрабатывает начальное накопление данных.
- Индикатор сравнения и скрипт проверки точки разрушения. RobustComparison совместно отображает классические и робастные полосы, рассчитанные по одному общему окну, позволяя увидеть разницу на свечном графике. RobustContaminationDemo измеряет точку разрушения на реальных ценовых данных и выводит результаты в виде таблицы.
Полученные результаты подтвердили то, что и требовалось показать в статье. На реальных данных EURUSD один искаженный бар из сорока изменил знак наклона МНК и увеличил стандартное отклонение на 145%, тогда как наклон Тейла — Сена и MAD оставались стабильными. Теоретическая точка разрушения медианы на уровне 50% проявилась именно там, где предсказывала математика: медиана оставалась почти неизменной до искажения половины данных окна и лишь тогда потеряла устойчивость. Оценки ведут себя именно так, как предсказывает робастная статистика.
Инженерные выводы выходят за рамки этих конкретных индикаторов. Используйте единый источник истины, чтобы результаты инструментов визуализации и любых других компонентов, использующих эти расчеты, не расходились; проверяйте код численных расчетов по заранее известным результатам, прежде чем строить на нем дальнейшие решения; учитывайте вычислительные затраты O(n^2) на точную оценку Тейла — Сена, чтобы осознанно выбирать размер окна. Прежде всего, понимайте, что доказывают полученные результаты, а что — нет: устойчивость этого набора инструментов к загрязнению данных подтверждена, но это статистическое свойство, а не обещание прибыли. Используйте эти компоненты как надежную основу для своих решений, заменяя ими среднее арифметическое или стандартное отклонение там, где их применение неявно предполагает отсутствие загрязнения данных. Прежде чем делать выводы для торговли, проверьте в своем терминале, как такая замена влияет на вашу стратегию.
| Файл | Тип | Описание |
|---|---|---|
| RobustStats.mqh | Библиотека | Класс CRobustStats: медиана, MAD, MadSigma, MadScore, наклон и свободный член Тейла — Сена, MAD остатков, а также классические аналоги (среднее арифметическое, StdDev, z-оценка, наклон и свободный член МНК); предусмотрен однопроходный метод ComputeAll() |
| RobustBands.mq5 | Индикатор | Полосы на уровнях медианы плюс-минус произведение k и 1,4826 * MAD — робастная замена полосам Боллинджера |
| RobustTrend.mq5 | Индикатор | Трендовый канал Тейла — Сена с полушириной на основе MAD остатков — робастная замена каналу линейной регрессии |
| RobustOscillator.mq5 | Индикатор | Осциллятор с нормированием на основе MAD (робастная z-оценка), отображаемый в отдельном подокне и заменяющий осциллятор z-оценки или сигма-осциллятор |
| RobustComparison.mq5 | Индикатор | Совместно отображает на ценовом графике классические полосы (среднее арифметическое плюс/минус StdDev) и робастные полосы (медиана плюс/минус 1,4826 * MAD), рассчитанные по одному общему окну, для наглядного сравнения |
| RobustContaminationDemo.mq5 | Скрипт | Измеряет точку разрушения на реальных данных графика: получает N цен закрытия, постепенно искажает их и выводит робастные и классические оценки в виде таблицы |
| MQL5.zip | Архив | Архив со всеми вышеперечисленными файлами. Распакуйте архив в каталог установки терминала, и каждый файл окажется на своем месте. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/23676
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Фильтрация новостей с помощью экономического календаря MetaTrader 5 и резервного варианта на основе CSV
Автоматизация торговых стратегий в MQL5 (Часть 52): Модель tCISD с SSMT и Quarterly Theory
Нейросети в трейдинге: Диффузионная генерация торговых планов (Планировщик)
Построение системы обнаружения дивергенций (Часть III): Советник Adaptive SuperTrend
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования