English
preview
Альтернатива тиковому объему: Измерение рыночной активности по интервалам ожидания между котировками

Альтернатива тиковому объему: Измерение рыночной активности по интервалам ожидания между котировками

MetaTrader 5 — Индикаторы |
29 0
Muhammad Minhas Qamar
Muhammad Minhas Qamar

Введение

На каждом графике MetaTrader 5 уже есть готовый показатель рыночной активности: тиковый объем — количество обновлений цены, поступивших за время формирования бара. Он точно отвечает на тот вопрос, для которого предназначен. Если за время формирования бара поступило 640 котировок, тиковый объем покажет 640. Проблема возникает, когда мы задаем ему другой вопрос: демонстрирует ли рынок необычную активность прямо сейчас?

Ответить на этот вопрос по количеству котировок за бар мешают три обстоятельства. Первое — время суток. Для EURUSD средний интервал между котировками в самые спокойные полчаса суток примерно в 47 раз больше, чем в самые активные полчаса, и эта закономерность повторяется каждый день. Поэтому гистограмма тикового объема в основном отражает время суток. Высокий столбец при открытии лондонской сессии обычно означает лишь то, что началась лондонская сессия. Второе — своевременность. Окончательное количество котировок за бар известно лишь после его закрытия, поэтому в середине бара доступно только промежуточное значение, которое нельзя сравнивать с итоговыми значениями соседних закрытых баров. Третье — временное разрешение. Два минутных бара с 20 котировками каждый выглядят одинаково независимо от того, поступали котировки равномерно или одним всплеском за последние три секунды.

В этой статье мы создадим индикатор, который напрямую отвечает на этот вопрос. Он использует интервалы между последовательными котировками вместо их количества за бар и нормирует интервалы на обычный внутридневной профиль, устраняя типичный ритм торгового дня. Оставшаяся составляющая моделируется с помощью модели авторегрессионной условной длительности (ACD) Энгла и Рассела (1998). В результате получается одно число, обновляемое при каждой котировке: 1 означает обычную для этого часа активность рынка, 2 — что модель ожидает поступления котировок вдвое чаще обычного, а 0,5 — вдвое реже обычного. Это сглаженная оценка недавнего темпа поступления котировок, а не их количество за последние несколько секунд.

Затем мы проверяем, полезен ли этот показатель для прогнозирования волатильности на ближайшие несколько минут. Недавняя динамика цены уже позволяет прогнозировать большую часть этой волатильности, поэтому главное — проверить, дает ли активность дополнительную информацию, если текущая волатильность уже учтена в прогнозе. Для всех шести символов как минимум один способ измерения активности дает небольшое, но статистически значимое улучшение прогноза. Сам индикатор ACD проходит проверку на пяти символах; для USDJPY улучшение слишком мало, чтобы отличить его от шума. Результат сравнения индикатора ACD с более простым подсчетом в реальном времени зависит от горизонта прогнозирования и рынка; мы приводим результаты обоих подходов такими, какими они получились. Все написано на чистом MQL5: библиотека из пяти файлов, два скрипта и индикатор.

Мы рассмотрим следующие темы:

  1. Что такое тик в этом потоке данных
  2. Ежедневный пульс рынка
  3. Активность порождает активность — модель длительности
  4. Оценка параметров модели и проверка ее результатов
  5. Одно число, обновляемое при каждой котировке
  6. Дает ли активность какую-либо информацию
  7. Индикатор на графике
  8. О чем не говорят числа
  9. Заключение


Что такое тик в этом потоке данных

Прежде чем что-либо моделировать, нужно точно определить, с какими исходными данными мы работаем. В потоке котировок для розничной торговли на рынке Forex или CFD тик — это изменение котировки: изменилось значение Bid, Ask или оба значения. Это не сделка. На счете, использованном для этой статьи, ни по одному из двенадцати исследованных символов (валютные пары, золото, нефть, фондовые индексы и биткоин) не поступало тиков с обновлением последней цены, а также тиков, в которых повторялись неизменные значения Bid и Ask. Следовательно, каждый измеряемый ниже интервал ожидания — это время между двумя изменениями котировки, и было бы неверно описывать его как время между сделками.

Сами временные метки корректны. Для каждого символа встречались все 1 000 возможных значений миллисекундной части временной метки, без признаков округления до целых секунд, и ни у одного тика временная метка не была раньше, чем у предыдущего. За тридцать дней, данные которых используются во всей статье, по EURUSD зарегистрировано 2 356 394 изменения котировок; средний интервал ожидания составил 737 миллисекунд, а коэффициент вариации — 3,1. Если бы поступление котировок описывалось пуассоновским процессом — независимые поступления с постоянной интенсивностью, — интервалы ожидания имели бы экспоненциальное распределение со стандартным отклонением, равным среднему значению, и коэффициентом вариации 1. Ниже под "случайными поступлениями" понимается именно этот эталон. Втрое большее значение коэффициента означает, что интервалы ожидания гораздо менее равномерны, чем при случайных поступлениях. Отчасти это объясняется внутридневным ритмом, который мы устраним в следующем разделе; оставшаяся неравномерность связана с кластеризацией.

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

  • Тики с одной и той же миллисекундной меткой. Модели длительности работают с положительными интервалами ожидания, а нулевой интервал — не просто очень короткий интервал. Это означает, что один всплеск поступления котировок представлен в потоке данных несколькими записями. Мы объединяем в одно событие последовательные тики с одинаковой миллисекундной меткой. Для EURUSD это затронуло 0,09 % тиков. Для золота это затронуло около 7% тиков, поэтому такая обработка выполняется по явному правилу, а не оставляется на волю случая.
  • Закрытый рынок. Выходные, ежедневный перерыв в торгах индексом или праздничный день приводят к интервалам ожидания длительностью в часы или дни. Такие интервалы не означают низкую активность рынка: если подать в модель хотя бы один интервал ожидания в 60 часов, она усвоит, что до следующей котировки остаются дни. Если интервал ожидания превышает порог, по умолчанию 5 минут, следующее событие помечается как начало новой сессии. Событие сохраняется, его интервал ожидания — нет, а модель начинает заново, устанавливая ожидаемую длительность на долгосрочном уровне.

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

//+------------------------------------------------------------------+
//| Fold, flag and append; returns the number of events created.     |
//| An out-of-order tick is merged, so no wait is ever negative.     |
//+------------------------------------------------------------------+
int CAcdEvents::AddTicks(const MqlTick &ticks[],const int count)
  {
   if(count<=0 || !Reserve(m_total+count))
      return 0;

   const int before=m_total;
   for(int i=0;i<count;i++)
     {
      const long t=(long)ticks[i].time_msc;
      m_ticks++;
      if(m_last_msc>=0 && t<=m_last_msc)
        {
         m_merged++;
         continue;
        }

      const bool start=(m_last_msc<0 || t-m_last_msc>m_gap_ms);
      m_time[m_total] =t;
      m_start[m_total]=(uchar)(start ? 1 : 0);
      if(start)
         m_sessions++;
      m_total++;
      m_last_msc=t;
     }
   return m_total-before;
  }

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

Quote timeline with waits, a burst, a folded tick and a session gap

Рис. 1. От котировок к событиям: интервалы ожидания между изменениями котировок, всплеск, два тика с одной миллисекундной меткой, объединенные в одно событие, и закрытие рынка, после которого начинается новая сессия

Здесь нужна еще одна проверка: она выявляет поток данных, поведение которого не соответствует рыночному. На том же счете котировки BTCUSD, по-видимому, поступали как по часам: почти каждый интервал ожидания был близок к четверти секунды, а коэффициент вариации составлял 0,36 — намного ниже значения 1, соответствующего случайным поступлениям. Низкий коэффициент показывает регулярность интервалов ожидания, но не объясняет ее причину, поэтому порог служит защитным фильтром, а не средством диагностики. Скрипт калибровки сначала вычисляет коэффициент и прекращает работу, если он ниже 0,7: в этом случае модель кластеризации в основном описывала бы регулярность поступлений.

//+------------------------------------------------------------------+
//| Is this a market at all? Waits far more regular than random      |
//| arrivals suggest a timer-driven feed, not worth fitting.         |
//+------------------------------------------------------------------+
bool DescribeFeed(const CAcdEvents &ev,double &mean_wait_ms)
  {
   mean_wait_ms=0.0;
   double s=0.0,s2=0.0;
   long   n=0;
   for(int i=1;i<ev.Total();i++)
     {
      if(ev.IsStart(i))
         continue;
      const double w=ev.Wait(i);
      s +=w;
      s2+=w*w;
      n++;
     }
   if(n<2)
      return false;
   const double mean=s/n;
   const double cv  =MathSqrt(MathMax(s2/n-mean*mean,0.0))/mean;
   mean_wait_ms=mean;

   PrintFormat("  ticks read       : %I64d, %.2f%% folded into same-millisecond events",
               ev.TicksRead(),100.0*ev.TicksMerged()/MathMax(ev.TicksRead(),1));
   PrintFormat("  events           : %d in %I64d session(s)",ev.Total(),ev.Sessions());
   PrintFormat("  from / to        : %s / %s",
               TimeToString((datetime)(ev.Time(0)/1000),TIME_DATE|TIME_MINUTES),
               TimeToString((datetime)(ev.Time(ev.Total()-1)/1000),TIME_DATE|TIME_MINUTES));
   PrintFormat("  mean wait        : %.1f ms, coefficient of variation %.3f",mean,cv);
   if(cv<0.7)
     {
      Print("  WARNING: waits are far more regular than random arrivals. This feed");
      Print("  looks timer-driven, and a clustering model would describe the timer.");
      return false;
     }
   return true;
  }

Поэтому BTCUSD исключается из дальнейшего анализа. Далее в статье используются шесть из оставшихся символов: три основные валютные пары (EURUSD, GBPUSD и USDJPY), золото, а также индексы US30 и SPX500. Они охватывают три класса активов, для которых предназначен индикатор. Все результаты ниже приводятся для каждого из шести символов, включая слабые результаты.


Ежедневный пульс рынка

Получив очищенный ряд интервалов ожидания, прежде всего нужно устранить уже известную составляющую. На рис. 2 показан обычный интервал ожидания между котировками в зависимости от времени суток для EURUSD и US30. Для EURUSD интервалы ожидания меняются от нескольких сотен миллисекунд ближе к вечеру по европейскому времени до примерно десяти секунд около полуночи по серверному времени. Эта картина повторяется каждый день. Она показывает, что обычно для соответствующего часа, а не то, необычен ли сегодняшний день; модель, не учитывающая эту закономерность, будет тратить всю свою память на повторное выявление открытия лондонской сессии.

Normal wait between quotes by time of day, EURUSD and US30

Рис. 2. Обычный интервал ожидания между изменениями котировок в зависимости от времени суток для EURUSD и US30; логарифмическая шкала

CAcdDiurnal строит этот профиль по средним интервалам ожидания в фиксированных временных группах внутри суток, по умолчанию длительностью 30 минут. При необходимости профиль хранится отдельно для каждого дня недели. Три детали определяют, приносит ли профиль пользу или, напротив, незаметно вредит.

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

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

//+------------------------------------------------------------------+
//| Interpolate between the bin centers either side of t; the ring   |
//| wraps at midnight within the same weekday.                       |
//+------------------------------------------------------------------+
double CAcdDiurnal::Value(const long t_msc) const
  {
   const double pos =((double)((t_msc/1000)%86400)+(t_msc%1000)/1000.0)/m_bin_sec-0.5;
   const double fl  =MathFloor(pos);
   const double frac=pos-fl;
   int b0=(int)fl;
   if(b0<0)
      b0+=m_bins;
   const int b1  =(b0+1)%m_bins;
   const int base=DayOf(t_msc)*m_bins;
   return (1.0-frac)*m_value[base+b0]+frac*m_value[base+b1];
  }

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

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

//+------------------------------------------------------------------+
//| Put adjusted waits on a mean of one, so 1 reads as "normal".     |
//+------------------------------------------------------------------+
double CAcdDiurnal::UnitMeanWaits(const CAcdEvents &ev,const int from,const int to)
  {
   if(!m_ready)
      return 0.0;
   const int a=(from<1 ? 1 : from);
   const int b=(to>=ev.Total() ? ev.Total()-1 : to);

   double sum=0.0;
   long   cnt=0;
   for(int i=a;i<=b;i++)
     {
      if(ev.IsStart(i))
         continue;
      sum+=ev.Wait(i)/Value(ev.Time(i-1));
      cnt++;
     }
   if(cnt<1000 || sum<=0.0)
      return 0.0;
   const double factor=sum/cnt;
   Scale(factor);
   return factor;
  }

В скорректированных интервалах ожидания сохраняется структура. На тестовом периоде EURUSD их автокорреляция при лаге 1 событие по-прежнему составляет 0,115, а при лаге 100 событий — 0,066. За периодом высокой активности следуют другие такие периоды, и эта зависимость значительно сильнее той, которую можно объяснить временем суток. Именно эту остаточную кластеризацию мы моделируем в следующем разделе.


Активность порождает активность — модель длительности

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

psi_i = omega + alpha * x_(i-1) + beta * psi_(i-1)
x_i   = psi_i * eps_i,   mean(eps) = 1

Здесь x — скорректированный интервал ожидания, alpha — вес только что завершившегося интервала ожидания, а beta — вес предыдущего ожидаемого значения. Их сумма — это персистентность: доля влияния всплеска, сохраняющаяся от одного события к следующему. Читатели, знакомые с моделью GARCH(1,1), сразу узнают эту рекурсию: в ней вместо квадратов доходностей используются интервалы ожидания. Неожиданная составляющая eps — то, что не удалось предсказать с помощью ожидаемого значения; она нормирована так, чтобы ее среднее равнялось 1.

Персистентность проще интерпретировать через период полураспада — число событий, после которого влияние всплеска на ожидаемое значение уменьшается вдвое. При персистентности 0,9975 период полураспада составляет 277 событий, что для EURUSD при среднем интервале ожидания соответствует чуть более чем трем минутам. Во время всплеска события поступают чаще, поэтому тот же период полураспада занимает меньше обычного времени.

Expected wait dropping through a burst and decaying back with its half-life

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

Обновление выполняется одной строчкой арифметических вычислений, общей для кода оценки параметров и индикатора активности в реальном времени.

//--- the one-step update, shared by the model and the live state
double AcdNextPsi(const SAcdParams &p,const double x_prev,const double psi_prev)
  {
   return p.omega+p.alpha*x_prev+p.beta*psi_prev;
  }

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

Модели также требуется распределение неожиданной составляющей, поскольку параметры оцениваются методом максимального правдоподобия. Мы предлагаем два варианта. Экспоненциальное распределение соответствует чисто случайным поступлениям и не требует дополнительного параметра. Распределение Вейбулла добавляет параметр формы k: при k меньше 1 как очень короткие, так и очень длинные интервалы ожидания встречаются чаще, чем при экспоненциальном распределении. Именно так выглядит поток котировок со всплесками. При k = 1 распределение Вейбулла в точности совпадает с экспоненциальным распределением.

//--- log of the gamma-function term that makes a Weibull unit-mean
double AcdLogScale(const double shape)
  {
   return MathGammaLog(1.0+1.0/shape);
  }

//--- unit-mean Weibull log-density; psi-free terms are passed in for speed
double AcdLogDensity(const double shape,const double log_shape,const double log_scale,
                     const double log_x,const double psi)
  {
   const double log_z=log_scale+log_x-MathLog(psi);
   return log_shape+shape*log_z-log_x-MathExp(shape*log_z);
  }

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


Оценка параметров модели и проверка ее результатов

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

//+------------------------------------------------------------------+
//| Build the adjusted waits. The profile is scaled first and each   |
//| wait divided once, so the live state matches bit for bit.        |
//+------------------------------------------------------------------+
bool CAcdModel::SetEvents(const CAcdEvents &ev)
  {
   m_total=ev.Total();
   if(m_total<5000)
     {
      PrintFormat("CAcdModel: %d events is too few to fit a duration model",m_total);
      m_total=0;
      return false;
     }
   m_split=(int)MathFloor(m_train_frac*m_total);

   if(!m_diurnal.Setup(m_bin_minutes,m_by_weekday))
      return false;
   if(!m_diurnal.FitWaits(ev,1,m_split-1))
      return false;

   if(m_diurnal.UnitMeanWaits(ev,1,m_split-1)<=0.0)
     {
      Print("CAcdModel: the training range holds too few usable waits");
      return false;
     }

   ArrayResize(m_x,m_total);
   ArrayResize(m_logx,m_total);
   ArrayResize(m_start,m_total);
   double sum=0.0;
   long   cnt=0;
   for(int i=0;i<m_total;i++)
     {
      if(i==0 || ev.IsStart(i))
        {
         m_x[i]    =0.0;
         m_logx[i] =0.0;
         m_start[i]=1;
         continue;
        }
      m_x[i]    =ev.Wait(i)/m_diurnal.Value(ev.Time(i-1));
      m_logx[i] =MathLog(m_x[i]);
      m_start[i]=0;
      if(i<m_split)
        {
         sum+=m_x[i];
         cnt++;
        }
     }
   m_mean=sum/cnt;
   ArrayFree(m_psi);
   return true;
  }

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

//+------------------------------------------------------------------+
//| Search vector to parameters; persistence stays below one.        |
//+------------------------------------------------------------------+
void CAcdModel::Decode(const double &u[],SAcdParams &p) const
  {
   double q=1.0/(1.0+MathExp(-u[0]));
   if(q>0.99999)
      q=0.99999;
   const double s=1.0/(1.0+MathExp(-u[1]));

   p.dist =m_dist;
   p.alpha=q*s;
   p.beta =q*(1.0-s);
   p.mean =m_mean;
   p.omega=(1.0-q)*m_mean;
   p.shape=1.0;
   if(m_dist==ACD_DIST_WEIBULL)
      p.shape=MathMax(0.05,MathMin(10.0,MathExp(u[2])));
  }

Симплекс максимизирует среднее логарифмическое правдоподобие на одно обучающее событие:

//+------------------------------------------------------------------+
//| Mean log-likelihood per usable event in [a,b]. The recursion runs|
//| from event 0 and restarts at the mean after each session start.  |
//+------------------------------------------------------------------+
double CAcdModel::MeanLogLik(const SAcdParams &p,const int a,const int b,int &n) const
  {
   const bool   expo     =(p.dist==ACD_DIST_EXPONENTIAL);
   const double log_shape=MathLog(p.shape);
   const double log_scale=(expo ? 0.0 : AcdLogScale(p.shape));

   double ll    =0.0;
   double psi   =p.mean;
   double x_prev=0.0;
   bool   fresh =true;
   n=0;

   for(int i=0;i<=b;i++)
     {
      if(m_start[i]!=0)
        {
         fresh=true;
         continue;
        }
      psi  =(fresh ? p.mean : AcdNextPsi(p,x_prev,psi));
      fresh=false;
      x_prev=m_x[i];
      if(i<a)
         continue;

      if(expo)
         ll+=-MathLog(psi)-m_x[i]/psi;
      else
         ll+=AcdLogDensity(p.shape,log_shape,log_scale,m_logx[i],psi);
      n++;
     }
   return (n>0 ? ll/n : 0.0);
  }

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

//+------------------------------------------------------------------+
//| Score a parameter set on both ranges, with its baseline.         |
//+------------------------------------------------------------------+
bool CAcdModel::Evaluate(const SAcdParams &p,SAcdFit &fit)
  {
   if(m_total==0)
      return false;

   fit.params  =p;
   fit.ll_train=MeanLogLik(p,0,m_split-1,fit.n_train);
   fit.ll_test =MeanLogLik(p,m_split,m_total-1,fit.n_test);

   SAcdParams base=p;
   base.alpha=0.0;
   base.beta =0.0;
   base.omega=p.mean;
   base.shape=(p.dist==ACD_DIST_WEIBULL ? FitBaseShape() : 1.0);

   int n=0;
   fit.base_shape   =base.shape;
   fit.base_ll_train=MeanLogLik(base,0,m_split-1,n);
   fit.base_ll_test =MeanLogLik(base,m_split,m_total-1,n);
   fit.evaluations  =0;
   fit.converged    =true;
   return true;
  }

Скрипт ACD_Calibrate выполняет все эти действия для одного символа и выводит результат. Для EURUSD за тридцать дней, закончившихся 14 сентября 2026 года, обучающий период включал 1 648 063 события, а тестовый — 706 313. Для модели с распределением Вейбулла получены alpha = 0,0210, beta = 0,9765 и параметр формы 0,795. По правдоподобию на отложенном периоде она превзошла модель с экспоненциальным распределением на 0,058 на событие. По сравнению с собственной базовой моделью, учитывающей только внутридневной ритм, выигрыш составил 0,034 на событие на обучающем периоде и 0,055 — на тестовом. На тестовом периоде выигрыш больше, вероятнее всего потому, что кластеризация на нем оказалась сильнее. Важно, что выигрыш сохранился вне обучающей выборки: для 706 313 тестовых событий он в сумме составляет около 39 000 единиц логарифмического правдоподобия. Это первый признак того, что модель выявила реальную кластеризацию, а не подстроилась под шум.

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

Ряд
Лаг 1
Лаг 2
Лаг 5
Лаг 20
Лаг 100
Q(100) Льюнга — Бокса
Скорректированные интервалы ожидания
+0,115
+0,129
+0,086
+0,079
+0,066
398 240
Неожиданные составляющие после оценки модели
+0,031
+0,044
+0,003
+0,001
-0,002
2,723

Таблица 1. Автокорреляция до и после оценки модели, тестовый период EURUSD

Начиная с лага 5 оставшиеся корреляции отличаются от нуля не более чем на несколько тысячных, а статистика Льюнга — Бокса снижается на 99,3%. Тест по-прежнему отвергает оцененную модель, как отверг бы почти любую модель на столь большой выборке. Поэтому его p-значение указывает на несовершенство модели, но не позволяет сравнивать качество разных оцененных моделей; для этого служит величина снижения статистики. Около 2 000 из оставшихся 2 723 приходятся только на лаги 1 и 2. Эти небольшие корреляции действительно существуют, и мы вернемся к ним в последнем разделе.

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

Символ
События
alpha
beta
Персистентность
Период полураспада, событий
Параметр Вейбулла k
Выигрыш на событие, тестовый период
EURUSD
2 354 376
0,0210
0,9765
0,99750
277
0,795
+0,055
GBPUSD 3 191 539
0,0376
0,9586
0,99625
184
0,815
+0,055
USDJPY
2 854 333
0,0443
0,9518
0,99615
180
0,818
+0,095
XAUUSD
13 567 262
0,0357
0,9584
0,99402
116
0,788
+0,062
US30
4 349 394
0,0228
0,9764
0,99920
870
0,736
+0,091
SPX500
2 870 082
0,0200
0,9788
0,99885
604
0,573
+0,074

Таблица 2. Результаты оценки ACD с распределением Вейбулла по котировкам за тридцать дней, закончившихся 14 сентября 2026 года

Выделяются две закономерности. Учет кластеризации дает преимущество перед базовой моделью, учитывающей только внутридневной ритм, для каждого символа вне обучающей выборки. Влияние всплеска сохраняется у двух американских индексов в пересчете на число событий в 2-7,5 раза дольше, чем у валютных пар и золота. При этом у SPX500 разброс неожиданных составляющих наибольший среди всех шести символов. Эпизоды активности котировок по этим индексам более длительны и неравномерны.


Одно число, обновляемое при каждой котировке

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

//+------------------------------------------------------------------+
//| One event: adjust the ended wait and fold it into psi. A wait    |
//| over the gap resets psi to the long-run mean, as in the fit.     |
//+------------------------------------------------------------------+
bool CAcdState::OnEvent(const long t_msc)
  {
   if(!m_ready || (m_last>=0 && t_msc<=m_last))
      return false;

   m_events++;
   if(m_last<0 || t_msc-m_last>m_gap_ms)
     {
      m_last    =t_msc;
      m_psi     =m_p.mean;
      m_x       =0.0;
      m_surprise=0.0;
      return true;
     }

   m_x       =(double)(t_msc-m_last)/m_diurnal.Value(m_last);
   m_surprise=m_x/m_psi;
   m_psi     =AcdNextPsi(m_p,m_x,m_psi);
   m_last    =t_msc;
   return true;
  }

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

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

//+------------------------------------------------------------------+
double CAcdState::ActivityRatio(void) const
  {
   return (m_psi>0.0 ? m_p.mean/m_psi : 0.0);
  }

//+------------------------------------------------------------------+
double CAcdState::ExpectedWaitMs(void) const
  {
   if(m_last<0)
      return 0.0;
   return m_psi*m_diurnal.Value(m_last);
  }

//+------------------------------------------------------------------+
double CAcdState::SilenceRatio(const long now_msc) const
  {
   const double expected=ExpectedWaitMs();
   if(expected<=0.0 || now_msc<m_last)
      return 0.0;
   return (double)(now_msc-m_last)/expected;
  }

На тестовом периоде EURUSD медиана коэффициента активности составила 1,08, 5-й процентиль — 0,54, а 95-й процентиль — 2,24. Для 7,6% событий темп активности рынка был как минимум вдвое выше обычного, а для 3,5% — не превышал половины обычного. Эти показатели рассчитываются по событиям, причем на активную минуту приходится больше событий, чем на спокойную, поэтому медиана немного выше 1.


Дает ли активность какую-либо информацию

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

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

  • A — обычная волатильность для этого времени суток.
  • B — A плюс реализованная волатильность последних 15 закрытых минутных баров, рассчитанная по тем же посекундным ценовым данным. Это то, что уже есть у любого трейдера.
  • C — B плюс тиковый объем за эти пятнадцать завершенных баров, деленный на его нормальный уровень для данного времени дня. Именно эта информация доступна индикатору, который пересчитывается при закрытии бара.
  • D — B плюс количество тиков в двух скользящих окнах, заканчивающихся в текущую секунду: за последнюю минуту и последние 15 минут. Каждое из двух значений делится на свой обычный уровень. Это простой альтернативный показатель, работающий в реальном времени.
  • E — B плюс коэффициент активности ACD и коэффициент тишины.
  • F — D и E вместе.

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

  • G — B плюс реализованная волатильность за последнюю минуту и последние 15 минут, рассчитанная по ценовым данным с шагом 5 секунд вплоть до текущей секунды. Это самая сильная базовая модель в данной последовательности: только ценовая информация, столь же свежая, как показатели активности в реальном времени.
  • H — G плюс коэффициент активности ACD и коэффициент тишины.
  • I — G плюс два показателя количества тиков в скользящих окнах.

E и H используют два показателя, рассчитанных по состоянию ACD, но на графике отображается только первый, поэтому на двух дополнительных ступенях используется лишь он:

  • J — B плюс только коэффициент активности ACD.
  • K — G плюс только коэффициент активности ACD.

D, E и G добавляют к B по два числовых показателя, а H и I — по два к G, поэтому показатели реального времени всегда сравниваются на равных условиях; C, J и K добавляют по одному. Все значения R² также рассчитываются на тестовом периоде, где неинформативный показатель обычно снижает результат, а не повышает его.

//--- the columns each rung regresses on
int RungColumns(const int r,int &cols[])
  {
   ArrayResize(cols,NCOL);
   cols[0]=COL_TOD;
   cols[1]=COL_RV;
   switch(r)
     {
      case 0:
         return 1;
      case 1:
         return 2;
      case 2:
         cols[2]=COL_BAR;
         return 3;
      case 3:
         cols[2]=COL_LIVE1;
         cols[3]=COL_LIVEW;
         return 4;
      case 4:
         cols[2]=COL_ACT;
         cols[3]=COL_SIL;
         return 4;
      case 5:
         cols[2]=COL_LIVE1;
         cols[3]=COL_LIVEW;
         cols[4]=COL_ACT;
         cols[5]=COL_SIL;
         return 6;
      case 6:
         cols[2]=COL_RV1;
         cols[3]=COL_RVW;
         return 4;
      case 7:
         cols[2]=COL_RV1;
         cols[3]=COL_RVW;
         cols[4]=COL_ACT;
         cols[5]=COL_SIL;
         return 6;
      case 8:
         cols[2]=COL_RV1;
         cols[3]=COL_RVW;
         cols[4]=COL_LIVE1;
         cols[5]=COL_LIVEW;
         return 6;
      case 9:
         cols[2]=COL_ACT;
         return 3;
      default:
         cols[2]=COL_RV1;
         cols[3]=COL_RVW;
         cols[4]=COL_ACT;
         return 5;
     }
  }

Целевая переменная — логарифм реализованной дисперсии средней цены между Bid и Ask за следующий горизонт прогнозирования. Все данные берутся из одного и того же потока котировок. Скрипт один раз загружает тики, затем формирует события для модели и посекундные ряды средней цены между Bid и Ask и количества тиков для регрессий. История баров вообще не используется. R² на тестовом периоде рассчитывается относительно среднего этого же периода, поэтому прогноз должен превзойти ориентир, которому средняя волатильность прогнозируемого периода уже известна. Далее один пункт соответствует 0,01 коэффициента R².

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

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

   for(int s0=w_sec+60;s0+60+h_sec<sec.total;s0+=60)
     {
      const int  s   =s0+(st.mid_bar ? MidBarOffset(base+s0) : 0);
      const long t_ms=(base+s)*1000;
      while(e<ev.Total() && ev.Time(e)<t_ms)
         state.OnEvent(ev.Time(e++));

      const long end_ms=t_ms+(long)h_sec*1000;
      if(t_ms<split_msc && end_ms>split_msc)
         continue;
      if(!state.InSession(t_ms) || sec.mid[s0-w_sec-1]<=0.0)
         continue;
      //--- no closed market anywhere from the history's start to the target's end
      const int ia=ev.FindTime((base+s0-w_sec-60)*1000);
      const int ib=ev.FindTime(end_ms);
      if(ib<=ia+1 || starts[ib]-starts[ia+1]>0 || ev.Time(ib-1)<end_ms-gap_ms)
         continue;

      double rv=0.0;
      for(int k=w_sec;k>0;k-=60)
        {
         const double r=MathLog(sec.mid[s0-k+59]/sec.mid[s0-k-1]);
         rv+=r*r;
        }
      //--- the same volatility, live: sampled like the target, up to this second
      double rv1=0.0,rvw=0.0;
      for(int j=InpStepSec;j<=w_sec;j+=InpStepSec)
        {
         const double r=MathLog(sec.mid[s-1-j+InpStepSec]/sec.mid[s-1-j]);
         rvw+=r*r;
         if(j<=60)
            rv1+=r*r;
        }
      double fut=0.0;
      for(int j=1;j<=n_steps;j++)
        {
         const double r=MathLog(sec.mid[s-1+j*st.step]/sec.mid[s-1+(j-1)*st.step]);
         fut+=r*r;
        }

      const long snap=base+s;
      const long m   =base+s0;
      g_t[n]  =t_ms;
      g_fut[n]=fut;
      g_rv[n] =rv;
      g_r1[n] =rv1;
      g_rw[n] =rvw;
      g_bar[n]=MathLog((sec.cum[s0]-sec.cum[s0-w_sec]+1.0)/(NormalTicks(per_minute,m-w_sec,m)+1.0));
      g_l1[n] =MathLog((sec.cum[s]-sec.cum[s-60]+1.0)/(NormalTicks(per_minute,snap-60,snap)+1.0));
      g_lw[n] =MathLog((sec.cum[s]-sec.cum[s-w_sec]+1.0)/(NormalTicks(per_minute,snap-w_sec,snap)+1.0));
      g_act[n]=MathLog(MathMax(state.ActivityRatio(),1.0e-6));
      g_sil[n]=MathLog(state.SilenceRatio(t_ms)+0.05);
      if(t_ms<split_msc)
         n_train++;
      n++;
     }

Прежде чем считать эти различия в R² подтверждением результата, нужно выполнить еще один шаг. Снимки создаются с интервалом в минуту, но горизонт прогнозирования достигает 15 минут, поэтому соседние ошибки прогноза относятся к сильно перекрывающимся целевым периодам, а персистентность волатильности продлевает эту зависимость. Поэтому каждое значимое для исследования сравнение проверяется тестом Диболда — Мариано, который определяет, можно ли отличить от нуля среднюю разность квадратов ошибок двух моделей. При оценке дисперсии используются веса Ньюи — Уэста на протяжении 60 снимков — примерно часа, что в несколько раз превышает максимальную длительность перекрытия. Значение t-статистики выше примерно 2 указывает на то, что выигрыш вряд ли случаен. Если ограничить лаг горизонтом прогнозирования, как обычно делают для перекрывающихся прогнозов, значения статистики будут выше; мы приводим более консервативные оценки.

//+------------------------------------------------------------------+
//| Diebold-Mariano t on squared test errors, positive when a beats  |
//| b. Newey-West variance, because the target windows overlap.      |
//+------------------------------------------------------------------+
double DmStat(const double &err[],const int n_test,const int a,const int b,const int lag)
  {
   double d[];
   ArrayResize(d,n_test);
   double mean=0.0;
   for(int i=0;i<n_test;i++)
     {
      const double ea=err[a*n_test+i],eb=err[b*n_test+i];
      d[i]=eb*eb-ea*ea;
      mean+=d[i];
     }
   mean/=n_test;
   double var=0.0;
   for(int i=0;i<n_test;i++)
      var+=(d[i]-mean)*(d[i]-mean);
   for(int l=1;l<=lag && l<n_test;l++)
     {
      double g=0.0;
      for(int i=l;i<n_test;i++)
         g+=(d[i]-mean)*(d[i-l]-mean);
      var+=2.0*(1.0-l/(lag+1.0))*g;
     }
   var/=n_test;
   return (var>0.0 ? mean/MathSqrt(var/n_test) : 0.0);
  }

Та же последовательность регрессионных моделей проверяется в трех режимах. Сначала — ровно в начале каждой минуты с горизонтом 15 минут: когда количество тиков закрытого бара уже известно полностью и показатель по барам находится в наиболее выгодных условиях. Затем — еще в двух режимах, на меняющейся секунде внутри каждой минуты, с горизонтами 5 минут и 1 минута: именно для таких моментов предназначен индикатор активности в реальном времени.

При закрытии бара показатели активности дают одинаковый результат.

Символ
A
B
C
D
E
F
G
H
I
EURUSD
0,435
0,531
0,569
0,573
0,574
0,582
0,558
0,580
0,576
GBPUSD 0,465
0,558
0,586
0,590
0,586
0,597
0,586
0,600
0,602
USDJPY
-0,846
0,461
0,468
0,470
0,469
0,470
0,503
0,502
0,503
XAUUSD
0,376
0,497
0,537
0,544
0,532
0,545
0,543
0,554
0,559
US30
0,565
0,620
0,667
0,672
0,670
0,676
0,648
0,676
0,677
SPX500
0,449
0,571
0,643
0,654
0,650
0,660
0,622
0,660
0,658

Таблица 3. R² на тестовом периоде: прогноз в начале каждой минуты на следующие 15 минут

Активность явно имеет значение: добавление любого из трех показателей активности к модели B улучшает прогноз на 3-8 пунктов для каждого символа, кроме USDJPY, где выигрыш составляет менее одного пункта. Однако после закрытия бара эти три показателя несут практически одинаковую информацию, а различия между C, D и E не превышают примерно одного пункта. Различие между E и C — именно эта пара проверялась — статистически незначимо для всех символов; наибольшее значение t-статистики составляет 1,7. Это ожидаемый результат, а не повод для разочарования. При закрытии бара количество тиков закрытых баров известно полностью, поэтому показатель по барам находится в наиболее выгодных условиях. Текущая волатильность в этот момент тоже не добавляет более свежей информации: для EURUSD и обоих индексов G уступает любому из показателей активности.

Отрицательное значение R² модели A для USDJPY — не ошибка. Уровень волатильности на тестовом периоде отличался от уровня на обучающем, а профиль времени суток сам по себе не может учесть такой сдвиг; после включения недавней волатильности в B качество прогноза восстанавливается.

Внутри бара показатели в реальном времени выходят вперед.

Символ
B
C
D
E
F
G
H
I
EURUSD
0,552
0,585
0,593
0,599
0,601
0,619
0,632
0,625
GBPUSD 0,523
0,555
0,566
0,569
0,578
0,606
0,620
0,617
USDJPY
0,557
0,571
0,585
0,583
0,587
0,628
0,631
0,631
XAUUSD
0,550
0,570
0,629
0,614
0,631
0,636
0,651
0,658
US30
0,661
0,702
0,737
0,746
0,747
0,756
0,778
0,771
SPX500
0,545
0,587
0,639
0,664
0,660
0,677
0,701
0,685

Таблица 4. R² на тестовом периоде: прогноз внутри бара на следующие 5 минут

Символ
B
C
D
E
F
G
H
I
EURUSD
0,413
0,447
0,468
0,457
0,466
0,473
0,485
0,490
GBPUSD 0,373
0,403
0,431
0,420
0,432
0,447
0,460
0,466
USDJPY
0,464
0,486
0,511
0,498
0,510
0,527
0,532
0,538
XAUUSD
0,439
0,441
0,523
0,506
0,529
0,540
0,554
0,564
US30
0,579
0,606
0,658
0,664
0,666
0,681
0,699
0,696
SPX500
0,424
0,446
0,512
0,535
0,526
0,551
0,571
0,563

Таблица 5. R² на тестовом периоде: прогноз внутри бара на следующую минуту

Из таблиц 4 и 5 можно сделать три вывода.

Каждый показатель в реальном времени превосходит тиковый объем закрытых баров. Внутри бара E превосходит C для всех шести символов на обоих горизонтах на 1,0-8,8 пункта; наибольший выигрыш наблюдается для золота и индексов. Различие статистически значимо в 11 из 12 случаев; исключение — GBPUSD на горизонте 5 минут (t-статистика 1,6).

Более свежие ценовые данные обеспечивают большую часть этого выигрыша. Добавление только текущей волатильности (ступень G) дает более высокий результат, чем добавление только показателей ACD (ступень E), для каждого символа на обоих горизонтах — на 1-4,5 пункта. Значительная часть преимущества показателя активности в реальном времени перед данными закрытых баров объясняется просто свежестью информации; ценовой показатель в реальном времени также использует свежие данные.

Активность все же дает информацию, которой нет в ценах. Если текущая волатильность уже учтена в прогнозе, добавление любого из двух показателей активности повышает R² на тестовом периоде для всех шести символов на обоих горизонтах. В таблице 6 отдельно приведены эти выигрыши и соответствующие им t-статистики.

Символ
Индикатор активности ACD, 5 мин
Количество тиков в скользящих окнах, 5 мин
Индикатор активности ACD, 1 мин
Количество тиков в скользящих окнах, 1 мин
EURUSD
+0,012 (2,6)
+0,005 (1,9)
+0,012 (2,5)
+0,016 (3,6)
GBPUSD +0,013 (2,8)
+0,010 (1,8)
+0,012 (3,3)
+0,019 (3,4)
USDJPY
+0,003 (1,3)
+0,002 (1,4)
+0,006 (1,9)
+0,011 (3,0)
XAUUSD
+0,015 (3,5)
+0,021 (3,2)
+0,014 (4,6)
+0,023 (5,5)
US30
+0,022 (4,6)
+0,015 (3,9)
+0,019 (5,1)
+0,015 (4,5)
SPX500
+0,024 (6,3)
+0,008 (5,6)
+0,019 (6,0)
+0,011 (4,7)

Таблица 6. Выигрыш в R² на тестовом периоде от добавления активности к текущей волатильности внутри бара; в скобках — t-статистика Диболда — Мариано: H минус G для индикатора активности ACD, I минус G для количества тиков в скользящих окнах

What activity adds to live volatility, per symbol and horizon

Рис. 4. Вклад каждого показателя активности в прогноз внутри бара при уже учтенной текущей волатильности, для каждого символа и горизонта

Выигрыш невелик. Для индикатора ACD выигрыш составляет долю пункта на USDJPY, 1-1,5 пункта на других валютных парах и золоте и около двух пунктов на индексах. Выигрыш статистически значим для всех символов, кроме USDJPY, где t-статистика составляет 1,3 на горизонте 5 минут и 1,9 на горизонте 1 минута. Выигрыш от количества тиков в скользящих окнах статистически значим на горизонте 1 минута для всех символов, включая USDJPY, а на горизонте 5 минут — только для золота и индексов.

Этот выигрыш дает именно показатель, отображаемый линией на графике. Результаты модели K, в которой исключен коэффициент тишины, отличаются от результатов H не более чем на 0,001 для всех символов на обоих горизонтах; t-статистики отличаются от значений H не более чем на 0,3. Коэффициент тишины не дал измеримого вклада.

То, какой показатель активности вносит больший вклад, зависит от горизонта и рынка, и не каждое сравнение дает однозначный результат. При прямом сравнении H и I на горизонте 5 минут индикатор ACD дает статистически значимо больший выигрыш, чем количество тиков в скользящих окнах, для EURUSD и обоих индексов и меньший — для золота; для GBPUSD и USDJPY различий не выявлено. На горизонте 1 минута количество тиков в скользящих окнах дает статистически значимо больший выигрыш для GBPUSD, USDJPY и золота, а индикатор — для SPX500; для EURUSD и US30 различий не выявлено. Сравнение E и D без текущей волатильности дает такое же соотношение результатов, а объединение показателей в модели F добавляет не более примерно двух пунктов по сравнению с использованием только количества тиков в скользящих окнах.

Итог таков: большая часть улучшения по сравнению с данными закрытых баров объясняется работой в реальном времени, причем свежие ценовые данные дают больший выигрыш, чем активность. Активность все же стоит измерять: для каждого исследованного символа хотя бы один показатель активности содержал статистически значимую информацию о ближайших нескольких минутах, которой не давала текущая волатильность. Индикатор ACD — хороший, но не единственный способ измерения активности; лучше всего он проявляет себя на горизонте 5 минут и на индексах. Для прогнозов на одну минуту по валютным парам и золоту скорректированное на внутридневной ритм количество тиков в скользящих окнах дало не худшие результаты. Для индикатора ACD не нужно выбирать окно подсчета, а память модели определяется по данным, а не задается вручную.


Индикатор на графике

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

   SAcdFit best=fe;
   if(fw.ll_test>fe.ll_test)
      best=fw;
   PrintFormat("  held-out log-likelihood prefers the %s density",
               (best.params.dist==ACD_DIST_WEIBULL ? "Weibull" : "exponential"));
   ReportResiduals(model,best);

   Print("--- parameters for the live state ------------------");
   PrintFormat("  %s  gap %d s  bins %d min%s",AcdParamsToString(best.params),InpGapSec,
               InpBinMinutes,(InpByWeekday ? " per weekday" : ""));

Во входные параметры индикатора вставляются только значения alpha и beta. Параметр формы распределения Вейбулла не нужен для работы в реальном времени: он входит в расчет правдоподобия, но не ожидаемой длительности.

При загрузке индикатор оценивает именно внутридневной ритм. Он оценивает профиль времени суток по 7 торговым дням перед отображаемым диапазоном, прогревает на них состояние, затем последовательно обрабатывает последние 3 торговых дня. Поэтому данные на графике не корректируются профилем, при построении которого использовались они сами.

Таким образом, ритм и параметры намеренно берутся из разных временных окон. Ритм меняется в зависимости от сезона и новостного календаря, поэтому индикатор оценивает его по данным последних дней. Параметры alpha и beta описывают, насколько долго сохраняется влияние всплесков после устранения ритма. Оба профиля нормируют скорректированные интервалы ожидания до среднего 1, поэтому параметры можно использовать с новым профилем. Индикатор использует один профиль для всех дней недели: при окне 7 дней на каждый день недели пришлось бы лишь один-два дня данных. Поэтому при калибровке оставляйте InpByWeekday равным false — значению по умолчанию.

//+------------------------------------------------------------------+
//| Learn the rhythm on the days before the plot, warm the state,    |
//| then replay the plotted days.                                    |
//+------------------------------------------------------------------+
bool Build(const datetime &time[],const long &tick_volume[],const int rates_total)
  {
   const datetime now        =TimeCurrent();
   const datetime plot_from  =TradingDateStart(time,rates_total,InpPlotDays);
   const datetime rhythm_from=TradingDateStart(time,rates_total,InpPlotDays+InpRhythmDays);
   if(plot_from<=0 || rhythm_from>=plot_from)
      return false;

   CAcdEvents ev;
   ev.SetGapSeconds(InpGapSec);
   if(!FeedRange(rhythm_from,plot_from,GetPointer(ev),time,tick_volume,rates_total))
      return false;
   if(ev.Total()<5000)
     {
      if(!g_waiting)
         PrintFormat("ACD_Activity: only %d quotes before %s, waiting for tick history",
                     ev.Total(),TimeToString(plot_from,TIME_DATE));
      return false;
     }
   if(!g_rhythm.Setup(InpBinMinutes,false) || !g_rhythm.FitWaits(ev,1,ev.Total()-1) ||
      g_rhythm.UnitMeanWaits(ev,1,ev.Total()-1)<=0.0)
      return false;
   if(!g_state.Init(g_params,GetPointer(g_rhythm),InpGapSec))
      return false;
   for(int i=0;i<ev.Total();i++)
      g_state.OnEvent(ev.Time(i));
   g_last_msc=ev.Time(ev.Total()-1);

   double sum=0.0;
   int    cnt=0;
   g_bar=-1;
   for(int i=0;i<rates_total;i++)
     {
      if(time[i]>=rhythm_from && time[i]<plot_from && tick_volume[i]>0)
        {
         sum+=(double)tick_volume[i];
         cnt++;
        }
      if(g_bar<0 && time[i]>=plot_from)
         g_bar=i;
     }
   if(g_bar<0)
      return false;
   g_vol_mean=(cnt>0 ? sum/cnt : 0.0);

   ArrayInitialize(BufActivity,EMPTY_VALUE);
   ArrayInitialize(BufColor,0.0);
   ArrayInitialize(BufVolume,EMPTY_VALUE);
   ArrayInitialize(BufRatio,EMPTY_VALUE);
   ArrayInitialize(BufExpected,EMPTY_VALUE);
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,g_bar);
   PlotIndexSetInteger(1,PLOT_DRAW_BEGIN,g_bar);

   return FeedRange(plot_from,now+60,NULL,time,tick_volume,rates_total);
  }

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

//+------------------------------------------------------------------+
//| Fold one tick in, closing every bar it has moved past.           |
//+------------------------------------------------------------------+
void FeedTick(const long t_msc,const datetime &time[],const long &tick_volume[],const int rates_total)
  {
   while(g_bar+1<rates_total && t_msc>=(long)time[g_bar+1]*1000)
     {
      WriteBar(g_bar,((long)time[g_bar]+PeriodSeconds())*1000,tick_volume);
      g_bar++;
     }
   g_state.OnEvent(t_msc);
   if(t_msc>g_last_msc)
      g_last_msc=t_msc;
  }
//+------------------------------------------------------------------+
//| Write the gauge for one bar; a closed market leaves a break.     |
//+------------------------------------------------------------------+
void WriteBar(const int i,const long now_msc,const long &tick_volume[])
  {
   BufExpected[i]=EMPTY_VALUE;
   if(!g_state.InSession(now_msc))
     {
      BufActivity[i]=EMPTY_VALUE;
      BufRatio[i]   =EMPTY_VALUE;
      BufVolume[i]  =EMPTY_VALUE;
      return;
     }
   const double ratio=g_state.ActivityRatio();
   BufRatio[i]   =ratio;
   BufActivity[i]=MathLog(MathMax(ratio,1.0e-6))/M_LN2;
   BufColor[i]   =(ratio>=InpBusy ? 1.0 : (ratio<=InpQuiet ? 2.0 : 0.0));
   BufVolume[i]  =(InpShowVolume && g_vol_mean>0.0 && tick_volume[i]>0
                   ? MathLog(tick_volume[i]/g_vol_mean)/M_LN2 : EMPTY_VALUE);
  }

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

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

ACD_Activity on EURUSD M1 through the London open

Рис. 5. ACD_Activity на EURUSD M1 в период открытия лондонской сессии: тиковый объем растет с началом сессии, а коэффициент активности остается близким к норме

На рис. 5 показано различие, наиболее важное при повседневном использовании. При открытии лондонской сессии тиковый объем резко возрастает, поскольку с ее началом всегда поступает больше котировок. Линия активности практически не изменяется, поскольку такой рост как раз соответствует норме для этого часа. За три торговых дня, показанных на графике EURUSD M1, время суток объясняет 72,8% изменчивости линии тикового объема, но лишь 29,7% изменчивости линии активности. Эта доля не снижается до нуля, поскольку ритм оценивался по 7 дням перед отображаемым периодом, а не по самим показанным дням.

ACD_Activity lighting up during a genuine burst of quotes

Рис. 6. Настоящий всплеск: коэффициент активности за считанные секунды поднимается в зону высокой активности еще до закрытия бара

На рис. 6 показан другой случай. Когда котировки внезапно начинают поступать значительно чаще обычного для этого часа, линия активности становится оранжево-красной уже через несколько секунд после начала всплеска. Бар при этом еще формируется, а его тиковый объем пока отражает лишь промежуточное количество котировок.

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

//--- indicator buffer map, fixed for EAs reading it through iCustom
enum ENUM_ACD_BUFFER
  {
   ACD_BUF_ACTIVITY = 0,               // log2 of the activity ratio, plotted
   ACD_BUF_COLOR    = 1,               // 0 normal, 1 busy, 2 quiet
   ACD_BUF_VOLUME   = 2,               // log2 of tick volume against its average
   ACD_BUF_RATIO    = 3,               // the activity ratio itself
   ACD_BUF_EXPECTED = 4                // forming bar only: expected wait in ms for the next quote
  };

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

   const int edge=rates_total-1;
   while(g_bar<edge)
     {
      WriteBar(g_bar,((long)time[g_bar]+PeriodSeconds())*1000,tick_volume);
      g_bar++;
     }
   const long now_msc=MathMax((long)TimeCurrent()*1000,g_last_msc);
   WriteBar(edge,now_msc,tick_volume);
   if(BufRatio[edge]!=EMPTY_VALUE)
      BufExpected[edge]=g_state.ExpectedWaitMs();
   return rates_total;

Это значение предоставляется вместо готового коэффициента тишины: индикатор, созданный через iCustom, не получает событий таймера, поэтому обновляемое между котировками значение не доходило бы до советника. Советник самостоятельно вычисляет коэффициент тишины. Серверное время изменяется с шагом в целую секунду, чего недостаточно при ожидаемой длительности в секунду или меньше. Поэтому советнику следует сохранять значение GetTickCount64 при поступлении каждого нового тика и при обработке событий своего таймера делить число прошедших с того момента миллисекунд на значение из буфера 4. Значение, значительно превышающее 1, указывает, что следующая котировка уже запаздывает.

Логичное применение — управление исполнением, а не выбором направления: повременить с отправкой нового ордера, когда активность рынка в несколько раз выше обычной, а спреды и проскальзывание часто особенно велики. В этой статье спреды не измерялись, поэтому такое применение следует протестировать на собственном счете.


О чем не говорят числа

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

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

Текущая волатильность измеряется одним способом. Модель G использует цены с шагом 5 секунд, тогда как B использует минутные бары. Часть преимущества G перед показателями активности может объясняться более частым получением ценовых данных, а не только их свежестью; при другом способе оценки волатильности вклад активности мог бы быть меньше или больше.

Распределение Вейбулла лишь приблизительно описывает форму распределения интервалов ожидания. Для EURUSD коэффициент вариации неожиданных составляющих составил 1,53, тогда как оцененный параметр формы подразумевает 1,27: у распределения реальных интервалов ожидания хвосты тяжелее, чем допускает распределение Вейбулла. Форма распределения в основном влияет на сравнение правдоподобия, но alpha и beta тоже оценивались при этом предположении, поэтому более гибкое распределение могло бы немного изменить их значения.

На очень коротких лагах сохраняется некоторая зависимость. После оценки модели автокорреляция неожиданных составляющих по-прежнему составляет 0,03 и 0,04 на лагах 1 и 2 события. В потоках котировок есть закономерности на малых временных масштабах, например парные обновления Bid и Ask, поступающие с небольшим интервалом, которые нельзя учесть одним параметром персистентности.

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

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


Заключение

Мы начали с узкого вопроса: демонстрирует ли рынок необычную активность прямо сейчас? Тиковый объем отвечает на связанный, но другой вопрос — сколько котировок поступило за время формирования бара, — а большая часть его изменчивости определяется временем суток. Мы создали индикатор активности, который напрямую отвечает на первый вопрос при поступлении каждой котировки.

  • Интервалы ожидания, а не количество котировок. Использование интервалов между изменениями котировок, объединение тиков с одинаковой миллисекундной меткой и пометка периодов закрытия рынка дают модели очищенный ряд для любого символа, поток котировок которого проходит проверку на регулярность.
  • Сначала устраните ритм. Профиль времени суток, оцененный только по обучающему периоду, с интерполяцией между временными группами и нормировкой обычного уровня к 1 устраняет уже известную составляющую активности.
  • Постройте модель оставшейся составляющей. Рекурсия ACD учитывает кластеризацию, оставшуюся после устранения ритма: она превзошла базовую модель, учитывающую только внутридневной ритм, вне обучающей выборки на всех шести символах и устранила 99,3% автокорреляции на EURUSD.
  • Активность дополняет свежие ценовые данные. Внутри бара индикатор активности в реальном времени улучшал прогнозы волатильности на горизонтах 1-5 минут по сравнению с тиковым объемом закрытых баров для каждого символа, хотя добавление только текущей волатильности давало больший выигрыш. Если текущая волатильность уже была учтена в прогнозе, индикатор все же давал небольшой, но статистически значимый выигрыш на пяти из шести символов. Его обеспечивал сам отображаемый на графике коэффициент активности; для USDJPY выигрыш был слишком мал, чтобы отличить его от шума. По сравнению с количеством тиков в скользящих окнах в реальном времени индикатор давал лучший результат на горизонте 5 минут для EURUSD и индексов и худший — для золота. На горизонте 1 минута преимущество было у подсчета тиков для валют и золота; в нескольких сравнениях различий не выявлено.

Чтобы использовать индикатор на своем счете:

  1. Запустите ACD_Calibrate для символа с данными котировок за 30 дней.
  2. Убедитесь, что поток данных прошел проверку на регулярность (иначе скрипт останавливается), а выигрыш от учета кластеризации в модели с распределением Вейбулла на тестовом периоде положителен.
  3. Вставьте выведенные значения alpha и beta во входные параметры ACD_Activity для этого символа.
  4. Проводите повторную калибровку примерно раз в месяц, а также при каждой смене символа или брокера. Если персистентность заметно меняется от запуска к запуску, значит прежние параметры устарели.
Программы, представленные в этой статье, предназначены исключительно для образовательных и исследовательских целей. Они измеряют рыночную активность и не являются торговыми рекомендациями или торговой системой. Прежде чем использовать их в торговле реальными средствами, тщательно протестируйте любой вариант применения на демо-счете.


Получение исходного кода через MQL5 Algo Forge

Все исходные файлы прикреплены ниже к этой статье, но полный репозиторий также доступен на MQL5 Algo Forge — платформе сообщества на базе Git для публикации торговых проектов и совместной работы над ними.

Имя файла
Описание
MQL5\Include\ACD\AcdTypes.mqh
Общие типы: параметры, отчет об оценке модели, схема буферов, одношаговое обновление и логарифм плотности
MQL5\Include\ACD\AcdEvents.mqh
Преобразует поток тиков в события: объединение тиков с одинаковой миллисекундной меткой, разрывы между сессиями, загрузка данных по дням
MQL5\Include\ACD\AcdDiurnal.mqh
Профиль времени суток с интерполяцией и нормировкой скорректированных интервалов к среднему 1
MQL5\Include\ACD\AcdModel.mqh
Оценка параметров методом максимального правдоподобия, базовая модель, учитывающая только внутридневной ритм, и диагностика остатков
MQL5\Include\ACD\AcdState.mqh
Состояние в реальном времени с обновлением за постоянное время: коэффициент активности, неожиданная составляющая, ожидаемая длительность и коэффициент тишины
MQL5\Scripts\ACD\ACD_Calibrate.mq5
Оценивает модель для одного символа и выводит результаты проверки потока данных, профиль ритма, результаты оценки обеих моделей, остатки и параметры для работы в реальном времени
MQL5\Scripts\ACD\ACD_Evidence.mq5
Мультисимвольная проверка активности как предиктора волатильности при закрытии бара и внутри бара
MQL5\Indicators\ACD\ACD_Activity.mq5
Индикатор активности в реальном времени, отображающий для сравнения тиковый объем и предоставляющий буферы для советников

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

Прикрепленные файлы |
MQL5.zip (28.98 KB)
От клонирования и коммитов к CI: Расширенные возможности MQL5 Algo Forge От клонирования и коммитов к CI: Расширенные возможности MQL5 Algo Forge
Мы разбираем возможности Algo Forge, важные для повседневной работы с MQL5: открытие репозиториев в MetaEditor, перевод README, вкладку Units, защиту веток и тегов, внесение правок в браузере, пакеты и CI на хосте с Windows. В статье приведены точные настройки и необходимые метки раннера, а также показано, как разрешать слияние (merge) только после успешной сборки. Вы получите воспроизводимые инструкции по защите кода, автоматизации компиляции и публикации артефактов.
Построение моделей волатильности в MQL5 (Часть III): Реализация алгоритма SLSQP для оценки моделей Построение моделей волатильности в MQL5 (Часть III): Реализация алгоритма SLSQP для оценки моделей
На языке MQL5 реализован оптимизатор SLSQP для устранения расхождений в параметрах между библиотекой волатильности и модулем ARCH для Python. В статье подробно описаны обработка ограничений, варианты вычисления градиента, настройка и средства управления сходимостью, а также показано, как интегрировать решатель в существующий код. Практические примеры и сравнения демонстрируют совпадение логарифмов правдоподобия и параметров на общих наборах данных.
Создание системы торговой аналитики (Часть 4): Сводные метрики и дашборд Создание системы торговой аналитики (Часть 4): Сводные метрики и дашборд
В этой статье мы расширяем существующий бэкенд Flask, чтобы рассчитывать аналитические показатели эффективности на основе сохраненных закрытых сделок MetaTrader 5 и предоставлять их как в формате JSON, так и в виде простого веб-представления. Система рассчитывает общее количество сделок, общую прибыль, долю прибыльных сделок, среднюю прибыль и метрики продолжительности сделок, возвращает JSON по адресу /api/v1/analytics/summary и отображает дашборд по адресу /api/v1. В результате появляется быстрый и единообразный способ анализировать эффективность торговли на основе сохраненных записей SQLite.
Разработка классического индикатора «Крестики-нолики» на языке MQL5 Разработка классического индикатора «Крестики-нолики» на языке MQL5
В данной статье на языке MQL5 реализован пользовательский индикатор «Крестики-нолики» (Point and Figure, PF), который отображает движение цены в столбцы X/O, используя фиксированный размер клетки и логику разворота через три клетки. Мы определяем базовую цену, преобразуем цены в интервалы клеток, управляем трендами и разворотами, автоматически масштабируем окно индикатора и отображаем символы с помощью объектов, обеспечивая четкое, независимое от времени представление трендов, прорывов и уровней поддержки и сопротивления.