English
preview
От одной цены к четырём: Методы оценки волатильности на основе ценового диапазона в MetaTrader 5

От одной цены к четырём: Методы оценки волатильности на основе ценового диапазона в MetaTrader 5

MetaTrader 5 — Трейдинг |
68 0
Adeolu Kayode Gbadebo
Adeolu Kayode Gbadebo

Содержание

  1. Введение
  2. Почему метод оценки по ценам закрытия отбрасывает три четверти данных каждого бара
  3. Четыре метода оценки: от наивного до устойчивого к ценовым разрывам
  4. Создание библиотеки: CVolatilityEstimators
  5. Индикатор 1: наглядное сравнение статистической эффективности
  6. Индикатор 2: полосы волатильности с учётом ценовых разрывов
  7. Крайние случаи и подводные камни
  8. Использование библиотеки в собственном коде
  9. Заключение


Введение

Почти каждый инструмент анализа волатильности, которым пользуется пользователь MetaTrader 5, использует лишь одно значение на бар. Полосы Боллинджера рассчитываются на основе стандартного отклонения цен закрытия. Средний истинный диапазон (ATR) по своей сути ближе к тому, что нам нужно, но это сглаженный диапазон, а не оценка дисперсии, и в качестве такой оценки его редко используют. Между тем каждый бар, отображаемый терминалом, содержит четыре цены: цену открытия, максимум, минимум и цену закрытия, причём максимум и минимум внутри бара несут много информации о том, насколько сильно в действительности двигалась цена. Отбрасывая их, мы теряем информацию, и эту потерю можно измерить.

В научной литературе этот вопрос был решен ещё несколько десятилетий назад. Семейство методов оценки волатильности на основе ценового диапазона, начало которому положил Паркинсон в 1980 году и которое усовершенствовали Гарман и Класс в том же году, затем Роджерс и Сатчелл и, наконец, Янг и Чжан в 2000 году, позволяет извлекать гораздо больше информации из тех же баров. При заданной длине окна хороший метод оценки волатильности на основе ценового диапазона достигает той же статистической точности, что и метод оценки по ценам закрытия, используя лишь часть данных, или, что равнозначно, позволяет на уже имеющемся окне получить гораздо более плавный ряд оценок волатильности, быстрее реагирующий на изменения. Однако в MetaTrader 5 они не представлены среди стандартных индикаторов, а в CodeBase нет аккуратной реализации этого классического семейства методов оценки, пригодной для повторного использования: поиск выдаёт модели GARCH и базовый ATR, но не реализацию, объединяющую метод оценки Паркинсона, метод оценки Гармана—Класса и метод оценки Янга—Чжана за единым интерфейсом.

В этой статье мы восполняем этот пробел. Мы создаём единую автономную библиотеку VolatilityEstimators.mqh, в которой реализованы четыре метода оценки волатильности — от простейшего базового до самого сложного. Затем мы используем эту библиотеку для создания двух индикаторов. Первый — сравнительная панель, на которой совместно отображаются результаты всех четырёх методов оценки, чтобы вы могли увидеть разницу в статистической эффективности на своих графиках. Второй — практический инструмент: полосы волатильности, ширина которых определяется с помощью устойчивого к ценовым разрывам метода оценки Янга—Чжана, а не стандартного отклонения цен закрытия. К концу статьи вы поймёте, что измеряет каждый метод оценки, почему они различаются и в каких случаях стоит выбирать каждый из них, а также получите рабочий код, который сможете использовать в своих проектах.

Вам не понадобятся ни внешние библиотеки, ни тиковые данные. Всё описанное здесь работает на стандартных барах OHLC (цены открытия, максимума, минимума и закрытия), а весь код компилируется в MetaEditor без каких-либо зависимостей, кроме самой платформы.

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


Почему метод оценки по ценам закрытия отбрасывает три четверти информации каждого бара

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

r_t = ln(C_t / C_{t-1})                 
sigma^2 = (1/(N-1)) * sum_{t=1..N} (r_t - r_bar)^2

Такой метод оценки корректен и несмещен, но использует данные неэкономно. При этом учитываются только цены закрытия. Если бар открылся на уровне 1.1000, резко поднялся до 1.1080, опустился до 1.0950 и закрылся на уровне 1.1005, то метод оценки по ценам закрытия видит лишь движение на пять пипсов. На самом деле цена в пределах бара прошла диапазон в 130 пипсов. Каждое из этих колебаний представляет собой реальную волатильность, а метод оценки не учитывает ни одного из них.

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

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

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

Один бар, четыре цены и информация, которую использует каждый метод оценки

Рис. 1. Четыре цены одной свечи плюс цена закрытия предыдущей свечи. Каждый метод оценки использует свое подмножество этих цен.


Четыре метода оценки: от наивного до устойчивого к ценовым разрывам

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

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

2. Оцениватель Паркинсона (1980): диапазон между максимумом и минимумом. Паркинсон показал, что при геометрическом броуновском движении без дрейфа математическое ожидание квадрата логарифмического диапазона бара пропорционально его дисперсии. Таким образом, сам диапазон служит методом оценки:

sigma^2 = (1/(4 N ln2)) * sum_{t=1..N} ln^2(H_t / L_t)

Коэффициент 1/(4 ln2) — это константа, после умножения на которую математическое ожидание квадрата диапазона становится равным дисперсии. Оцениватель Паркинсона обладает гораздо большей статистической эффективностью, чем метод оценки по ценам закрытия, однако это достигается за счёт двух допущений: цена движется по непрерывной траектории без дрейфа, а между барами нет скачков. Поскольку он не учитывает ни цену открытия текущего бара, ни цену закрытия предыдущего бара, он вообще не может обнаружить межсессионный ценовой разрыв. На рынке с ценовыми разрывами метод оценки Паркинсона систематически занижает волатильность, поскольку разрыв изменил цену, но не отразился в диапазоне между максимумом и минимумом ни одного отдельного бара.

3. Оцениватель Гармана—Класса (1980): весь бар. Гарман и Класс развили эту идею, задействовав данные всего бара: они объединили диапазон между максимумом и минимумом с движением цены от открытия до закрытия, чтобы добиться ещё большей статистической эффективности:

sigma^2 = (1/N) * sum_{t=1..N} [ 0.5 * ln^2(H_t / L_t) - (2 ln2 - 1) * ln^2(C_t / O_t) ]

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

4. Оцениватель Янга—Чжана (2000): независимость от дрейфа и ценовых разрывов. Янг и Чжан разработали метод оценки, который решает обе проблемы одновременно. Он представляет собой сумму трёх компонент: дисперсии межсессионной доходности, дисперсии доходности от открытия до закрытия и компоненты Роджерса—Сатчелла, которая сама по себе является удачно построенным методом оценки по движениям внутри бара и остаётся несмещенной при наличии дрейфа.

sigma^2 = sigma_overnight^2 + k * sigma_open-close^2 + (1 - k) * sigma_RS^2

Межсессионная компонента представляет собой дисперсию межсессионной доходности, компонента от открытия до закрытия — дисперсию доходности от открытия до закрытия, а компонента Роджерса—Сатчелла — среднее значение произведений величин внутри бара, определяемое следующим образом:

o_t = ln(O_t / C_{t-1})                                    // overnight return
u_t = ln(C_t / O_t)                                        // open-to-close return
RS_t = ln(H_t/C_t)*ln(H_t/O_t) + ln(L_t/C_t)*ln(L_t/O_t)   // Rogers-Satchell term
k = 0.34 / (1.34 + (N+1)/(N-1))                            // blend weight

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

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

Оцениватель
Используемые цены
Учитывает дрейф?
Учитывает ценовые разрывы?
Относительная статистическая эффективность
Оцениватель по ценам закрытия
Только C
Да
Да (неявно)
1× (базовый уровень)
Оцениватель Паркинсона
H, L
Нет
Нет
~5 раз
Оцениватель Гармана—Класса
O, H, L, C
Нет
Нет
~7–8 раз
Оцениватель Янга—Чжана
O, H, L, C + C предыдущего бара
Да
Да
Высокая; метод оценки устойчив к ценовым разрывам

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

Рис. 2. От метода оценки по ценам закрытия до метода оценки Янга—Чжана: цены, которые использует каждый метод оценки, и допущение, от которого он отказывается.


Создание библиотеки: CVolatilityEstimators

Почему именно класс с буфером скользящего окна, а не свободные функции? Эти методы оценки не являются рекурсивными: каждый из них представляет собой функцию последних N завершенных баров, а не единого текущего состояния, обновляемого при поступлении каждого нового наблюдения. Мы могли бы реализовать их в виде свободных функций, работающих с массивами OHLC (цены открытия, максимума, минимума и закрытия), но всем им требуется одно и то же скользящее окно, а методу оценки по ценам закрытия и межсессионной компоненте метода оценки Янга—Чжана нужна также цена закрытия бара, непосредственно предшествующего окну. Если инкапсулировать окно в классе, владеющем своим буфером, методы оценки смогут совместно использовать это хранилище, вызывающему коду не придется выполнять арифметические операции с индексами, а у объекта будет четко определенный жизненный цикл. Класс хранит последние window + 1 баров: дополнительный бар нужен для получения цены закрытия предыдущего бара.

Это хранилище представляет собой кольцевой буфер, а не массив со сдвигом элементов: новый бар записывается в ячейку самого старого бара, а указатель головы кольцевого буфера смещается, поэтому добавление бара выполняется за время O(1) независимо от размера окна. Закрытый вспомогательный метод Slot преобразует логический индекс «i баров назад» в индекс физической ячейки, поэтому методы оценки читают данные окна в естественном порядке, а вся арифметика по модулю сосредоточена в одном месте. Ниже приведены заголовок файла и объявление класса.

//+------------------------------------------------------------------+
//|                                        VolatilityEstimators.mqh  |
//|   Range-based OHLC volatility estimators in one reusable library |
//|                                                                  |
//|   One source of truth for the classic historical-volatility      |
//|   estimators used by the visual indicators. Feed one bar per call|
//|   with Update(open,high,low,close); read any estimate back as a  |
//|   per-bar standard deviation (Value) or an annualized figure.    |
//|                                                                  |
//|   Unlike a recursive filter, every estimator here is *windowed*: |
//|   the result is a function of the last N complete bars. The class|
//|   keeps its bars in a fixed-size RING BUFFER, so pushing a new   |
//|   bar is O(1) (no array shift) and callers never manage index    |
//|   math.                                                          |
//|                                                                  |
//|   Formulas are the standard published definitions:               |
//|     Close-to-Close  classic log-return sample variance           |
//|     Parkinson       (1980)  high-low range                       |
//|     Garman-Klass    (1980)  full OHLC bar, no overnight gap      |
//|     Yang-Zhang      (2000)  drift- and gap-independent           |
//|   Each returns sigma per bar; annualize by sqrt(bars per year).  |
//+------------------------------------------------------------------+
#property copyright "Adeolu Kayode"
#property strict

#define VOL_LN2 0.69314718055994530942   // ln(2), used by Parkinson & Garman-Klass

Перечисление ENUM_VOL_METHOD позволяет вызывающему коду выбирать метод оценки с помощью одного входного параметра; этот подход используется в обоих индикаторах. Вспомогательная структура SVolEstimates объединяет все четыре оценки стандартного отклонения за один бар, поэтому индикатор, которому нужны результаты всех методов оценки сразу, может получить их за один проход по окну вместо четырех отдельных вызовов. Сам класс содержит четыре параллельных массива для хранения истории OHLC (цены открытия, максимума, минимума и закрытия), а также служебные целочисленные переменные.

//+------------------------------------------------------------------+
//| Method selector shared by the class and its callers. Passing an  |
//| enum (rather than four separate accessors) lets an indicator     |
//| switch estimator from a single input without branching by hand.  |
//+------------------------------------------------------------------+
enum ENUM_VOL_METHOD
  {
   VOL_CLOSE_TO_CLOSE = 0,  // Close-to-Close (naive log-return sigma)
   VOL_PARKINSON      = 1,  // Parkinson (high-low range)
   VOL_GARMAN_KLASS   = 2,  // Garman-Klass (full OHLC bar)
   VOL_YANG_ZHANG     = 3   // Yang-Zhang (gap- and drift-robust)
  };

//+------------------------------------------------------------------+
//| Bundle of all four per-bar sigmas from a single window pass.     |
//| ComputeAll() fills one of these so an indicator that needs every |
//| estimator (like the comparison panel) sweeps the window once     |
//| instead of four times.                                           |
//+------------------------------------------------------------------+
struct SVolEstimates
  {
   double            ctc;   // Close-to-Close sigma
   double            park;  // Parkinson sigma
   double            gk;    // Garman-Klass sigma
   double            yz;    // Yang-Zhang sigma
  };

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

Два разных способа индексации, намеренно разделенных. Индикаторные буферы платформы по умолчанию индексируются от самого старого бара к самому новому (индекс 0 соответствует самому старому бару). Во внутреннем буфере класса принят обратный порядок: индекс 0 соответствует самому новому бару, как и в обозначении x[n] — «n баров назад», обычно используемом для записи этих формул. Эти два способа индексации не смешиваются: индикатор передает бары в класс в порядке их следования на графике и получает обратно одно скалярное значение, поэтому внутренний порядок индексации класса остается скрытой деталью реализации. В частности, оба индикатора используют массивы open[], high[], low[] и close[] именно в том порядке, в котором их предоставляет функция OnCalculate (от самого старого бара к самому новому). Для этих массивов они не вызывают функцию ArraySetAsSeries. Вам не нужно самостоятельно устанавливать флаг временного ряда, а обратный порядок индексации внутри класса с ним не конфликтует.
private:
   double            m_open[];    // fixed ring storage, physical slots (see Slot())
   double            m_high[];    //   logical "i bars ago" is mapped by Slot(i)
   double            m_low[];     //   0 = most recent bar, up to window bars back
   double            m_close[];   //   the extra slot holds the lagged close
   int               m_window;    // number of bars each estimate is computed over
   int               m_cap;       // ring capacity = window + 1 (holds the lagged close)
   int               m_count;     // real bars pushed so far (for warm-up / Ready)
   int               m_head;      // physical slot that currently holds the most recent bar

//--- 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);
     }

//--- sample variance of two parallel accumulators (Bessel-corrected).
//--- Pass sum and sum-of-squares of n values; avoids a temp array.
   double            SampleVarFromSums(const double sum, const double sumsq, const int n) const
     {
      if(n < 2)
         return(0.0);
      double mean = sum / n;
      double ss   = sumsq - (double)n * mean * mean;
      if(ss < 0.0)
         ss = 0.0;
      return(ss / (n - 1));
     }

Открытые методы управления жизненным циклом класса соответствуют тому, как индикатор с ним работает: Init задает размер кольцевого буфера и сбрасывает индекс головы, Reset очищает накопленные бары без повторного выделения памяти (используется, когда индикатор повторно обрабатывает окно), Update добавляет один бар, а Ready сообщает, накоплено ли полное окно данных. Важны две детали. Сначала Init выделяет window + 1 ячеек: дополнительная ячейка хранит цену закрытия предыдущего бара. Во-вторых, Update проверяет входные данные, отклоняя любой бар с неположительной ценой или максимумом ниже минимума, и возвращает значение типа bool, чтобы вызывающий код мог определить, что некорректный бар был пропущен. Некорректные бары пропускаются, чтобы они не искажали результаты всех последующих вычислений логарифмов. Наряду с ReadyIntrabar есть и Ready: метод оценки Паркинсона и метод оценки Гармана—Класса не используют цену закрытия предыдущего бара, поэтому могут выдавать корректные оценки на один бар раньше, чем два метода оценки, которым эта цена нужна.

public:
                     CVolatilityEstimators() : m_window(20), m_cap(21), m_count(0), m_head(-1) {}

//--- set the estimation window (bars) and allocate the ring buffer
   void              Init(const int window)
     {
      m_window = (window < 2 ? 2 : window);
      m_cap    = m_window + 1;               // +1 bar supplies the previous close
      ArrayResize(m_open,  m_cap);
      ArrayResize(m_high,  m_cap);
      ArrayResize(m_low,   m_cap);
      ArrayResize(m_close, m_cap);
      ArrayInitialize(m_open,  0.0);
      ArrayInitialize(m_high,  0.0);
      ArrayInitialize(m_low,   0.0);
      ArrayInitialize(m_close, 0.0);
      m_count = 0;
      m_head  = -1;                           // no bar yet; first Update() lands in slot 0
     }

//--- discard accumulated bars but keep the window/allocation. Cheap
//--- reset used when replaying a fresh window without reallocating.
   void              Reset()
     {
      m_count = 0;
      m_head  = -1;
     }

//--- feed one completed bar; call once per bar in chart order.
//--- Malformed bars (non-positive price, or high<low) are rejected
//--- so a single bad tick cannot poison the logarithms downstream.
//--- Returns true if the bar was accepted.
   bool              Update(const double o, const double h, const double l, const double c)
     {
      if(o <= 0.0 || h <= 0.0 || l <= 0.0 || c <= 0.0 || h < l)
         return(false);
      m_head = (m_head + 1) % m_cap;          // advance ring head, overwriting oldest slot
      m_open[m_head]  = o;
      m_high[m_head]  = h;
      m_low[m_head]   = l;
      m_close[m_head] = c;
      if(m_count < m_cap)
         m_count++;
      return(true);
     }

//--- true once a full window (plus the lagged close) has been seen.
//--- window+1 bars are the requirement for the two estimators that
//--- reference the previous close (Close-to-Close, Yang-Zhang overnight).
   bool              Ready() const { return(m_count >= m_cap); }

//--- Parkinson and Garman-Klass need only 'window' bars (no lagged
//--- close). Expose it so a caller can start those two one bar early.
   bool              ReadyIntrabar() const { return(m_count >= m_window); }

Сами методы оценки. Каждый сначала проверяет готовность данных, а затем вычисляет значение по своей формуле непосредственно на данных кольцевого буфера через Slot. Оцениватель по ценам закрытия — самый простой: он накапливает сумму логарифмических доходностей и сумму их квадратов, а затем передает их в SampleVarFromSums. Обратите внимание на Slot(i + 1): этот вызов обращается к бару перед окном — именно поэтому в кольцевом буфере предусмотрена дополнительная ячейка.

//--- Close-to-Close: sample standard deviation of log returns
//--- r = ln(C_t / C_{t-1}) over the window. Accumulates sum and
//--- sum-of-squares in one pass, so no temporary array is allocated.
   double            CloseToClose() const
     {
      if(!Ready())
         return(0.0);
      double sum = 0.0, sumsq = 0.0;
      for(int i = 0; i < m_window; i++)          // i bars back within the window
        {
         double r = MathLog(m_close[Slot(i)] / m_close[Slot(i + 1)]);
         sum   += r;
         sumsq += r * r;
        }
      return(MathSqrt(SampleVarFromSums(sum, sumsq, m_window)));
     }

Оцениватель Паркинсона и метод оценки Гармана—Класса реализованы каждый одним циклом накопления, построчно воспроизводящим соответствующую формулу; VOL_LN2 используется для вычисления нормирующего множителя 4 ln2 и веса (2 ln2 - 1). Оба метода оценки выполняют вычисления только после успешной проверки ReadyIntrabar, поскольку ни один из них не использует цену закрытия предыдущего бара; метод оценки Гармана—Класса извлекает квадратный корень только из положительной дисперсии, защищаясь от редких отрицательных значений, возникающих из-за шума в данных.

//--- Parkinson (1980): uses the high-low range of each bar.
   double            Parkinson() const
     {
      if(!ReadyIntrabar())
         return(0.0);
      double sum = 0.0;
      for(int i = 0; i < m_window; i++)
        {
         int s = Slot(i);
         double hl = MathLog(m_high[s] / m_low[s]);
         sum += hl * hl;
        }
      return(MathSqrt(sum / (4.0 * m_window * VOL_LN2)));
     }

//--- Garman-Klass (1980): combines the high-low range with the
//--- open-close move, using the whole bar.
   double            GarmanKlass() const
     {
      if(!ReadyIntrabar())
         return(0.0);
      double sum = 0.0;
      for(int i = 0; i < m_window; i++)
        {
         int s = Slot(i);
         double hl = MathLog(m_high[s] / m_low[s]);
         double co = MathLog(m_close[s] / m_open[s]);
         sum += 0.5 * hl * hl - (2.0 * VOL_LN2 - 1.0) * co * co;
        }
      double var = sum / m_window;
      return(var > 0.0 ? MathSqrt(var) : 0.0);
     }

Оцениватель Янга—Чжана имеет более сложную структуру. За один проход по окну он накапливает межсессионные доходности, доходности от открытия до закрытия и сумму значений компоненты Роджерса—Сатчелла, а затем объединяет три дисперсии с использованием веса k. Именно этот метод вызывает индикатор полос волатильности.

//--- Yang-Zhang (2000): the only one of the four that is both drift
//--- and gap-independent. One pass accumulates the overnight and
//--- open-to-close variances (via running sums) and the Rogers-
//--- Satchell term, so no temporary arrays are allocated.
   double            YangZhang() const
     {
      if(!Ready())
         return(0.0);

      double on_sum = 0.0, on_sq = 0.0;   // overnight   ln(O_t / C_{t-1})
      double oc_sum = 0.0, oc_sq = 0.0;   // open-close  ln(C_t / O_t)
      double rs     = 0.0;                // Rogers-Satchell (mean-of-products form)

      for(int i = 0; i < m_window; i++)
        {
         int s  = Slot(i);
         int sp = Slot(i + 1);            // previous bar (the lagged close)

         double on = MathLog(m_open[s]  / m_close[sp]);
         double oc = MathLog(m_close[s] / m_open[s]);
         on_sum += on;
         on_sq  += on * on;
         oc_sum += oc;
         oc_sq  += oc * oc;

         double hc = MathLog(m_high[s] / m_close[s]);
         double ho = MathLog(m_high[s] / m_open[s]);
         double lc = MathLog(m_low[s]  / m_close[s]);
         double lo = MathLog(m_low[s]  / m_open[s]);
         rs += hc * ho + lc * lo;
        }
      double sigma_on = SampleVarFromSums(on_sum, on_sq, m_window);   // overnight variance
      double sigma_oc = SampleVarFromSums(oc_sum, oc_sq, m_window);   // open-to-close variance
      double sigma_rs = rs / m_window;                                // Rogers-Satchell variance

      double k = 0.34 / (1.34 + (double)(m_window + 1) / (m_window - 1));
      double var = sigma_on + k * sigma_oc + (1.0 - k) * sigma_rs;
      return(var > 0.0 ? MathSqrt(var) : 0.0);
     }

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

//--- Compute all four estimators for the current window. Parkinson,
//--- Garman-Klass and Yang-Zhang share a single sweep of the bars;
//--- Close-to-Close (which needs only the closes) is folded into the
//--- same loop. One pass instead of four separate ones.
   bool              ComputeAll(SVolEstimates &out) const
     {
      out.ctc = 0.0;
      out.park = 0.0;
      out.gk = 0.0;
      out.yz = 0.0;
      if(!Ready())
         return(false);

      double c_sum = 0.0, c_sq = 0.0;     // close-to-close log returns
      double on_sum = 0.0, on_sq = 0.0;   // Yang-Zhang overnight
      double oc_sum = 0.0, oc_sq = 0.0;   // Yang-Zhang open-to-close
      double park = 0.0;                  // Parkinson high-low accumulator
      double gk   = 0.0;                  // Garman-Klass accumulator
      double rs   = 0.0;                  // Rogers-Satchell accumulator

      for(int i = 0; i < m_window; i++)
        {
         int s  = Slot(i);
         int sp = Slot(i + 1);

         double hl = MathLog(m_high[s] / m_low[s]);
         double co = MathLog(m_close[s] / m_open[s]);
         double r  = MathLog(m_close[s] / m_close[sp]);
         double on = MathLog(m_open[s]  / m_close[sp]);

         c_sum  += r;
         c_sq  += r * r;
         on_sum += on;
         on_sq += on * on;
         oc_sum += co;
         oc_sq += co * co;

         park += hl * hl;
         gk   += 0.5 * hl * hl - (2.0 * VOL_LN2 - 1.0) * co * co;

         double hc = MathLog(m_high[s] / m_close[s]);
         double ho = MathLog(m_high[s] / m_open[s]);
         double lc = MathLog(m_low[s]  / m_close[s]);
         double lo = MathLog(m_low[s]  / m_open[s]);
         rs += hc * ho + lc * lo;
        }

      out.ctc  = MathSqrt(SampleVarFromSums(c_sum, c_sq, m_window));
      out.park = MathSqrt(park / (4.0 * m_window * VOL_LN2));

      double gk_var = gk / m_window;
      out.gk = (gk_var > 0.0 ? MathSqrt(gk_var) : 0.0);

      double sigma_on = SampleVarFromSums(on_sum, on_sq, m_window);
      double sigma_oc = SampleVarFromSums(oc_sum, oc_sq, m_window);
      double sigma_rs = rs / m_window;
      double k = 0.34 / (1.34 + (double)(m_window + 1) / (m_window - 1));
      double yz_var = sigma_on + k * sigma_oc + (1.0 - k) * sigma_rs;
      out.yz = (yz_var > 0.0 ? MathSqrt(yz_var) : 0.0);
      return(true);
     }

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

//--- per-bar sigma for the requested estimator
   double            Value(const ENUM_VOL_METHOD method) const
     {
      switch(method)
        {
         case VOL_CLOSE_TO_CLOSE:
            return(CloseToClose());
         case VOL_PARKINSON:
            return(Parkinson());
         case VOL_GARMAN_KLASS:
            return(GarmanKlass());
         case VOL_YANG_ZHANG:
            return(YangZhang());
        }
      return(0.0);
     }

//--- scale a per-bar sigma to an annualized figure (barsPerYear e.g. 252)
   double            Annualized(const ENUM_VOL_METHOD method, const double barsPerYear) const
     {
      return(Value(method) * MathSqrt(barsPerYear));
     }

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


Индикатор 1: наглядное сравнение статистической эффективности

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

Визуальное оформление тоже помогает объяснить материал. Блок #property задает отдельное окно и четыре линейных графика с продуманным оформлением: метод оценки по ценам закрытия показан приглушенным серебристым цветом, чтобы его шум воспринимался как серая дымка; два метода оценки, использующих движения внутри бара, — цветами одинаковой визуальной значимости; а метод оценки Янга—Чжана — линией удвоенной толщины, на которую следует ориентироваться. На готовом графике видна одна сильно колеблющаяся серая линия и три более спокойные цветные линии.

#property indicator_separate_window
#property indicator_buffers 4
#property indicator_plots   4

#property indicator_label1  "Close-to-Close"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrSilver
#property indicator_width1  1

#property indicator_label2  "Parkinson"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrMediumSeaGreen
#property indicator_width2  1

#property indicator_label3  "Garman-Klass"
#property indicator_type3   DRAW_LINE
#property indicator_color3  clrGoldenrod
#property indicator_width3  1

#property indicator_label4  "Yang-Zhang"
#property indicator_type4   DRAW_LINE
#property indicator_color4  clrDodgerBlue
#property indicator_width4  2

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

#include <Volatility/VolatilityEstimators.mqh>

input int    InpWindow      = 20;     // Estimation window (bars)
input double InpBarsPerYear = 252.0;  // Bars per year for annualization (0 = raw per-bar sigma)

double CtcBuffer[];   // Close-to-Close sigma
double ParkBuffer[];  // Parkinson sigma
double GkBuffer[];    // Garman-Klass sigma
double YzBuffer[];    // Yang-Zhang sigma

CVolatilityEstimators g_vol;   // rolling-window estimator engine

Функция OnInit привязывает каждый буфер к индексу графика, настраивает размер окна метода оценки и задает краткое имя индикатора. Краткое имя — это имя индикатора, отображаемое в MetaTrader; если задать его с помощью IndicatorSetString так, чтобы оно содержало размер окна, можно с первого взгляда определить, с какими параметрами работает данный экземпляр. Мы также задаем точность отображения в пять знаков после запятой, что подходит для небольших значений стандартного отклонения, приведенных к годовому масштабу.

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   SetIndexBuffer(0, CtcBuffer,  INDICATOR_DATA);
   SetIndexBuffer(1, ParkBuffer, INDICATOR_DATA);
   SetIndexBuffer(2, GkBuffer,   INDICATOR_DATA);
   SetIndexBuffer(3, YzBuffer,   INDICATOR_DATA);

   g_vol.Init(InpWindow);
   IndicatorSetString(INDICATOR_SHORTNAME, StringFormat("VolCompare(%d)", InpWindow));
   IndicatorSetInteger(INDICATOR_DIGITS, 5);
   return(INIT_SUCCEEDED);
  }

Объем вычислений определяется значением prev_calculated, поэтому при каждом вызове выполняется лишь необходимый минимум работы. При первом проходе расчет выполняется для каждого бара; при каждом последующем тике пересчитывается только конечная часть данных, причем расчет начинается на один бар раньше, чтобы обновить последний, возможно еще не закрытый, бар. Кольцевой буфер заново заполняется исходными данными за window + 1 баров, предшествующих этой точке, поэтому уже для первого бара, результат которого записывается, доступно полное окно данных; один вызов ComputeAll на бар позволяет получить все четыре оценки за один проход. На этапе начального накопления данных, пока окно еще не заполнено, значения для баров устанавливаются в EMPTY_VALUE.

//+------------------------------------------------------------------+
//| Incremental calculation driven by prev_calculated.               |
//|                                                                  |
//| On the first pass (or a history refresh) prev_calculated is 0    |
//| and we recompute the whole series. On every later tick only the  |
//| tail has changed, so we recompute from 'start': the previously   |
//| last bar (which may have been unclosed) plus everything after it.|
//| The ring buffer is re-seeded with the window+1 bars that precede |
//| 'start' so the first written bar already has a full window, then |
//| a single ComputeAll() per bar fills all four estimators at once. |
//+------------------------------------------------------------------+
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 + 2)
      return(0);

//--- annualization scale (1.0 keeps the raw per-bar sigma when disabled)
   double scale = (InpBarsPerYear > 0.0 ? MathSqrt(InpBarsPerYear) : 1.0);

//--- first bar we must (re)write: the previously last bar, or 0 on a fresh pass
   int start = (prev_calculated > 0 ? prev_calculated - 1 : 0);

//--- re-seed the ring with the (window+1) bars preceding 'start' so the
//--- window is already full when we begin writing. 'seed' is clamped to 0.
   int seed = start - (InpWindow + 1);
   if(seed < 0)
      seed = 0;

   g_vol.Reset();
   for(int i = seed; i < start; i++)
      g_vol.Update(open[i], high[i], low[i], close[i]);

   SVolEstimates e;
   for(int i = start; i < rates_total; i++)
     {
      g_vol.Update(open[i], high[i], low[i], close[i]);
      if(g_vol.ComputeAll(e))
        {
         CtcBuffer[i]  = e.ctc  * scale;
         ParkBuffer[i] = e.park * scale;
         GkBuffer[i]   = e.gk   * scale;
         YzBuffer[i]   = e.yz   * scale;
        }
      else
        {
         CtcBuffer[i]  = EMPTY_VALUE;
         ParkBuffer[i] = EMPTY_VALUE;
         GkBuffer[i]   = EMPTY_VALUE;
         YzBuffer[i]   = EMPTY_VALUE;
        }
     }
   return(rates_total);
  }

На графике результат наглядно подтверждает все сказанное.

Графики четырех методов оценки волатильности в одном отдельном окне

Рис. 3. VolatilityComparison на [SYMBOL, TIMEFRAME]. Оцениватель по ценам закрытия (серебристая линия) дает самые шумные оценки; методы оценки волатильности на основе ценового диапазона дают более плавные кривые; метод оценки Янга—Чжана (синяя линия) показывает рост при ценовых разрывах.


Индикатор 2: полосы волатильности с учетом ценовых разрывов

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

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

#property indicator_chart_window
#property indicator_buffers 3
#property indicator_plots   3

#property indicator_label1  "Upper"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrCrimson
#property indicator_width1  1

#property indicator_label2  "Middle"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrDimGray
#property indicator_width2  1
#property indicator_style2  STYLE_DOT

#property indicator_label3  "Lower"
#property indicator_type3   DRAW_LINE
#property indicator_color3  clrCrimson
#property indicator_width3  1

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

#include <Volatility/VolatilityEstimators.mqh>

input int    InpWindow     = 20;    // Volatility window (bars)
input int    InpMAPeriod   = 20;    // Center moving-average period (bars)
input double InpMultiplier = 2.0;   // Band width in standard deviations

double UpperBuffer[];   // center * exp(+mult*sigma)
double MiddleBuffer[];  // simple moving average of close (channel center)
double LowerBuffer[];   // center * exp(-mult*sigma)

CVolatilityEstimators g_vol;   // rolling-window estimator engine
int g_warmup;                  // first bar index with both MA and volatility ready

Функция OnInit привязывает три буфера и вычисляет границу начального накопления данных. Индикатор не может отображать линии, пока не будут готовы оба компонента: оценка волатильности (для которой нужны бары в количестве InpWindow + 1) и скользящая средняя (готовая на баре с индексом InpMAPeriod - 1; отсюда и асимметрия выражений, поскольку речь идет об индексах баров, а не об их количестве). Функция MathMax выбирает большее из двух значений: первый бар, на котором возможно построение, определяется компонентом, для которого данные накапливаются позже, поэтому полосы никогда не строятся с еще не сформированной центральной линией или шириной.

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   SetIndexBuffer(0, UpperBuffer,  INDICATOR_DATA);
   SetIndexBuffer(1, MiddleBuffer, INDICATOR_DATA);
   SetIndexBuffer(2, LowerBuffer,  INDICATOR_DATA);

   g_vol.Init(InpWindow);
   g_warmup = MathMax(InpWindow + 1, InpMAPeriod - 1);   // both inputs must be filled
   IndicatorSetString(INDICATOR_SHORTNAME,
                      StringFormat("VolBands(%d,%d,%.1f)", InpWindow, InpMAPeriod, InpMultiplier));
   IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
   return(INIT_SUCCEEDED);
  }

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

//+------------------------------------------------------------------+
//| Simple moving average of the InpMAPeriod closes ending at i,     |
//| computed directly. Used once per recalculated tail to seed the   |
//| rolling sum; the loop below then maintains it in O(1) per bar.   |
//+------------------------------------------------------------------+
double SmaSum(const double &close[], const int i, const int period)
  {
   double sum = 0.0;
   for(int k = 0; k < period; k++)
      sum += close[i - k];
   return(sum);
  }

Полный расчет приведен ниже. Структура расчета повторяет структуру индикатора сравнения: ход расчета определяется значением prev_calculated, пересчитывается только конечный участок начиная с start, а кольцевой буфер метода оценки Янга—Чжана заново заполняется исходными данными предшествующих window + 1 баров. Здесь значение start также ограничивается снизу границей начального накопления данных, поэтому ничего не отображается, пока не накопятся все данные как для окна расчета волатильности, так и для скользящей средней. Наряду с кольцевым буфером один раз инициализируется сумма для расчета скользящей средней с помощью SmaSum, а затем эта сумма обновляется при переходе к каждому следующему бару. Построение полос в нижней части кода заслуживает особого внимания.

//+------------------------------------------------------------------+
//| Incremental calculation driven by prev_calculated.               |
//|                                                                  |
//| Only the tail is recomputed on each tick: from 'start' (the      |
//| previously last bar, possibly unclosed) to the end. The Yang-    |
//| Zhang ring buffer is re-seeded with the window+1 preceding bars, |
//| and the center's moving-average sum is kept as a rolling sum so  |
//| each bar costs O(1) instead of re-summing InpMAPeriod closes.    |
//+------------------------------------------------------------------+
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 < g_warmup + 1)
      return(0);

//--- first bar we must (re)write; never before the warm-up boundary
   int start = (prev_calculated > 0 ? prev_calculated - 1 : 0);
   if(start < g_warmup)
      start = g_warmup;

//--- re-seed the Yang-Zhang ring with the (window+1) bars before 'start'
   int seed = start - (InpWindow + 1);
   if(seed < 0)
      seed = 0;
   g_vol.Reset();
   for(int i = seed; i < start; i++)
      g_vol.Update(open[i], high[i], low[i], close[i]);

//--- rolling moving-average sum of the InpMAPeriod closes ending at 'start'
   double ma_sum = SmaSum(close, start, InpMAPeriod);

   for(int i = start; i < rates_total; i++)
     {
      if(i > start)
         ma_sum += close[i] - close[i - InpMAPeriod];

      g_vol.Update(open[i], high[i], low[i], close[i]);

      if(!g_vol.Ready())
        {
         UpperBuffer[i]  = EMPTY_VALUE;
         MiddleBuffer[i] = EMPTY_VALUE;
         LowerBuffer[i]  = EMPTY_VALUE;
         continue;
        }

      //--- center line and the per-bar Yang-Zhang sigma (a log-return sd)
      double center = ma_sum / InpMAPeriod;
      double sigma  = g_vol.YangZhang();
      double offset = InpMultiplier * sigma;

      //--- exact multiplicative bands: sigma is a log-return sd, so the
      //--- price offset is center*(exp(+/-offset)-1). This keeps the upper
      //--- and lower bands correctly asymmetric in price at larger sigma.
      MiddleBuffer[i] = center;
      UpperBuffer[i]  = center * MathExp(offset);
      LowerBuffer[i]  = center * MathExp(-offset);
     }
   return(rates_total);
  }

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

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

Полосы волатильности на основе метода оценки Янга—Чжана, наложенные на ценовой график вблизи межсессионного ценового разрыва

Рис. 4. VolatilityBands на [SYMBOL, TIMEFRAME]. Канал расширяется уже на самом баре с ценовым разрывом.

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

Полосы на основе метода оценки Янга—Чжана в сравнении со стандартными полосами Боллинджера вблизи одного и того же ценового разрыва

Рис. 5. Полосы на основе метода оценки Янга—Чжана (красные) в сравнении с полосами Боллинджера (синими), настройки одинаковы. Полосы на основе метода оценки Янга—Чжана расширяются на баре с ценовым разрывом; полосы Боллинджера реагируют на несколько баров позже.


Крайние случаи и подводные камни

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

Коэффициент приведения к годовому масштабу не универсален. В обоих индикаторах по умолчанию задано значение 252 — число торговых дней в году; оно корректно только тогда, когда каждый бар соответствует одному торговому дню. При приведении к годовому масштабу сигма одного бара умножается на квадратный корень из числа баров в году, поэтому для графика H1 на рынке, где торги идут круглосуточно пять дней в неделю, правильное значение коэффициента ближе к 6240 (24 × 5 × 52), а не к 252. Если оставить значение 252 на внутридневном графике, абсолютные значения теряют смысл, хотя относительное сравнение методов оценки по-прежнему корректно, поскольку все полученные оценки умножаются на одну и ту же константу. Задайте этот параметр осознанно: укажите число баров в году для используемого таймфрейма либо установите 0, чтобы получать сигму одного бара без приведения к годовому масштабу.

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

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

Начальное накопление данных: окно плюс один бар. Оба индикатора не выводят значений, пока Ready не вернет true; для этого требуется окно из window + 1 бара, поскольку нужна цена закрытия предыдущего бара. На новом графике с небольшим объемом истории или при очень большом размере окна следует ожидать пустого участка слева. Это нормальное поведение, а не ошибка.

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

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


Использование библиотеки в собственном коде

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

#include <Volatility/VolatilityEstimators.mqh>

CVolatilityEstimators vol;

int OnInit()
  {
   vol.Init(20);                     // 20-bar estimation window
   return(INIT_SUCCEEDED);
  }

//--- call once per completed bar (e.g. from an OnTick new-bar guard)
void OnNewBar()
  {
//--- feed the last CLOSED bar (shift 1), never the forming bar (shift 0)
   vol.Update(iOpen(_Symbol, _Period, 1),
             iHigh(_Symbol, _Period, 1),
             iLow(_Symbol, _Period, 1),
             iClose(_Symbol, _Period, 1));

   if(vol.Ready())
     {
      double yz = vol.YangZhang();       // per-bar sigma
      // use yz as a volatility-regime filter, or scale position size by 1/yz
     }
  }

В этом шаблоне важны два момента. Во-первых, передавайте классу данные последнего закрытого бара (сдвиг 1), а не формирующегося бара (сдвиг 0), чтобы каждая оценка строилась на основе окончательных значений OHLC (цены открытия, максимума, минимума и закрытия) и не колебалась внутри бара. Во-вторых, вызывайте метод Update ровно один раз на бар — это обеспечивает проверка появления нового бара в OnTick; если нужна оценка конкретного метода оценки, а не метода оценки Янга—Чжана, вызывайте Value(VOL_GARMAN_KLASS) и т. п. либо Annualized(method, barsPerYear) для получения сопоставимой оценки, приведенной к годовому масштабу. Именно это единственное возвращаемое число и требуется фильтру рыночного режима или алгоритму расчета размера позиции обратно пропорционально волатильности.

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

Рынок или сценарий применения
Рекомендуемый метод оценки
Дневные данные по акциям, индексам или фьючерсам с межсессионными ценовыми разрывами
Оцениватель Янга—Чжана
Внутридневные данные Форекс или любого рынка с практически непрерывной торговлей
Оцениватель Гармана—Класса или метод оценки Янга—Чжана (их оценки почти совпадают)
Базовый показатель, сопоставимый с классической исторической волатильностью
Оцениватель по ценам закрытия
Самый простой показатель, учитывающий только ценовой диапазон
Оцениватель Паркинсона

К этой таблице нужна одна оговорка: метод оценки Янга—Чжана не всегда оказывается лучшим. На действительно непрерывных внутридневных данных, где цена открытия каждого бара совпадает с ценой закрытия предыдущего, межсессионная компонента метода оценки Янга—Чжана почти ничего не вносит в результат, и его оценка сходится к оценке Гармана—Класса, так что дополнительная сложность расчета мало что дает. Кроме того, на очень коротких окнах оценки всех рассматриваемых здесь методов оценки подвержены сильному выборочному шуму — независимо от того, используют они ценовой диапазон или нет, — поскольку баров для усреднения просто недостаточно. Оцениватель Янга—Чжана — самый надежный вариант по умолчанию, но не универсальный победитель; все четыре метода оценки доступны через одно перечисление именно для того, чтобы для переключения достаточно было изменить один входной параметр.

Кратко о производительности. Три проектных решения снижают вычислительные затраты. Кольцевой буфер позволяет добавлять данные каждого бара за время O(1), без сдвига элементов массива. ComputeAll вычисляет все четыре оценки за один проход по окну вместо четырёх отдельных проходов. А параметр prev_calculated обеспечивает пересчет только конечной части данных, поэтому объем вычислений на каждом тике не растет с увеличением длины истории. Вместе они превращают класс из демонстрационного примера в инструмент, который можно спокойно оставлять работать на множестве графиков или в тестере стратегий.


Заключение

Мы поставили перед собой цель восполнить реальный пробел в наборе инструментов MetaTrader 5: реализовать классические методы оценки волатильности на основе ценового диапазона, широко известные в литературе, но отсутствующие на платформе. Мы создали VolatilityEstimators.mqh — единую библиотеку без зависимостей, реализующую метод оценки по ценам закрытия, метод оценки Паркинсона, метод оценки Гармана—Класса и метод оценки Янга—Чжана в одном классе со скользящим окном, предназначенном для повторного использования, а не только для демонстрации: кольцевой буфер с операциями за O(1), вычисление оценок без выделения памяти, однопроходный метод ComputeAll и проверку входных данных на границе интерфейса. Затем на основе этой библиотеки мы создали два индикатора с инкрементальным расчетом через prev_calculated: сравнительную панель, наглядно показывающую разницу в статистической эффективности, и набор полос волатильности с учетом ценовых разрывов, позволяющий применить на практике лучший из четырех методов оценки.

Главное здесь — не столько сам код, сколько логика: каждый метод оценки определяется лежащим в его основе допущением о рынке, а выбор подходящего метода оценки сводится к тому, чтобы это допущение соответствовало вашему инструменту. Оцениватель по ценам закрытия не использует часть информации, но не делает никаких допущений. Оцениватель Паркинсона и метод оценки Гармана—Класса отличаются высокой статистической эффективностью, но не учитывают ценовые разрывы и дрейф. Оцениватель Янга—Чжана немного уступает в максимальной статистической эффективности, но взамен обеспечивает устойчивость к обоим факторам, что делает его наиболее надежным выбором по умолчанию на реальных рынках с ценовыми разрывами и трендами; при этом для инструмента с почти непрерывной динамикой цены его результаты приближаются к результатам метода оценки Гармана—Класса, и выбор практически не имеет значения. Теперь у вас есть все четыре метода оценки в виде, позволяющем легко встроить их в любой индикатор, советник или исследовательский скрипт, а также понимание того, какой из них следует вызвать.

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


Файлы, прикрепленные к статье

Файл
Путь
Описание
VolatilityEstimators.mqh
MQL5/Include/Volatility/
Библиотека: четыре метода оценки волатильности на основе ценового диапазона, реализованные в одном классе со скользящим окном.
VolatilityComparison.mq5
MQL5/Indicators/Volatility/
Индикатор, отображающий в отдельном окне результаты всех четырех методов оценки одновременно для сравнения.
VolatilityBands.mq5
MQL5/Indicators/Volatility/
Индикатор в основном окне графика, отображающий полосы волатильности с учетом ценовых разрывов, ширина которых определяется с помощью метода оценки Янга—Чжана.
MQL5.zip
Каталог установки терминала
Архив со всеми вышеперечисленными файлами. Распакуйте его в каталог установки терминала, и все файлы будут размещены в нужных местах.

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

Прикрепленные файлы |
MQL5_zip.zip (8.98 KB)
Скрытые полумарковские модели (HSMM) на MQL5: Распознавание рыночных режимов с учетом их длительности Скрытые полумарковские модели (HSMM) на MQL5: Распознавание рыночных режимов с учетом их длительности
Стандартные HMM предполагают, что длительности состояний имеют геометрическое распределение со свойством отсутствия памяти, что плохо соответствует реальным рыночным фазам. В этой статье скрытая полумарковская модель (HSMM) с явным учетом длительности реализуется непосредственно на MQL5: для каждого состояния задается распределение времени пребывания в состоянии, а для фильтрации используется прямой фильтр по оставшейся длительности. Параметры оцениваются вне торговой среды с помощью алгоритма ожидания-максимизации (EM) и загружаются через компактный манифест в формате JSON. Торговый советник (EA) для XAUUSD на таймфрейме M5 использует ожидаемую оставшуюся длительность для фильтрации торговых решений о входе и выходе, что помогает удерживать позиции по тренду и избегать запоздалых входов перед завершением рыночного режима.
Конструирование признаков для машинного обучения (Часть 11): Фрактальные признаки на Python Конструирование признаков для машинного обучения (Часть 11): Фрактальные признаки на Python
В статье рассматривается конвейер для конструирования признаков на основе пятибарного фрактала Вильямса и показывается, как центрированное скользящее окно приводит к реальной утечке будущей информации. В статье выявлены две дополнительные скрытые ошибки — жестко заданный сдвиг, привязанный к значению n по умолчанию, и порог волатильности, игнорирующий переданное значение, — а исправления объединены под единым флагом leak_safe. Читатели получат фрактальные признаки, признаки уровней, тренда и сигналов без утечки будущей информации, а также рекомендации о том, в каких случаях столбцы без сдвига остаются пригодными для разметки.
Шероховатая волатильность: Индекс шероховатости на основе модели RFSV для фильтрации сделок с помощью машинного обучения Шероховатая волатильность: Индекс шероховатости на основе модели RFSV для фильтрации сделок с помощью машинного обучения
В этой статье мы строим практически применимый индекс шероховатости на основе теории шероховатой волатильности, оценивая локальное значение H с помощью регрессии по структурной функции на доходностях, агрегированных по блокам, — и всё это средствами MQL5. Мы объединяем его с признаками волатильности волатильности, обучаем классификатор на основе градиентного бустинга в Python, экспортируем модель в формат ONNX и вызываем её из торгового советника. Вы сможете вычислять H на каждом баре и использовать модель в качестве фильтра входа, учитывающего рыночный режим.
Теория случайных матриц (RMT): Подавление шума в корреляционной матрице для мультивалютных советников Теория случайных матриц (RMT): Подавление шума в корреляционной матрице для мультивалютных советников
Выборочные корреляционные матрицы могут казаться точными, но при этом в основном состоять из шума. В этой статье реализован алгоритм подавления шума на основе RMT на языке MQL5 без внешних зависимостей: спектральное разложение методом Якоби, отбор собственных значений по распределению Марченко — Пастура и восстановление матрицы с заменой шумовых собственных значений их средним, сохраняющее след матрицы и единичную диагональ. В статье объясняется, как интегрировать этот алгоритм в советник для торговли корзиной инструментов, чтобы очищенная матрица повышала стабильность коэффициентов хеджирования и весов между ребалансировками, а код оставался переносимым и доступным для аудита.