English
preview
Перекрестный рекуррентный количественный анализ (CRQA) в MQL5: Разработка полноценной библиотеки анализа

Перекрестный рекуррентный количественный анализ (CRQA) в MQL5: Разработка полноценной библиотеки анализа

MetaTrader 5 — Торговые системы |
56 0
Hammad Dilber
Hammad Dilber

Содержание

  1. Введение
  2. CRQA на графике
  3. От RQA к перекрёстной рекуррентности
  4. Матрица перекрёстной рекуррентности
  5. Метрики CRQA
  6. Эпсилон для двух рядов
  7. Архитектура библиотеки
  8. CCRQAMatrix: построение матрицы перекрёстной рекуррентности
  9. CCRQAMetrics: количественная оценка перекрёстной структуры
  10. Ускорение на графическом процессоре (GPU) с использованием OpenCL
  11. CCRQAWindow: перекрёстный анализ в скользящем окне
  12. CCRQA: фасад
  13. Индикатор CRQA
  14. Что дальше?
  15. Заключение


Введение

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

В этой статье ставится другой вопрос: как два ряда связаны между собой? Именно этим занимается перекрестный рекуррентный количественный анализ (Cross Recurrence Quantification Analysis, CRQA). Вместо того чтобы сравнивать ряд с самим собой, CRQA сравнивает траектории в фазовом пространстве двух отдельных рядов. В результате получается матрица перекрёстной рекуррентности, в которой в каждой ячейке фиксируется, близко ли состояние ряда X в момент времени i к состоянию ряда Y в момент времени j. Линейные структуры в этой матрице выявляют общую динамику, синхронизацию и связь между двумя системами.

Для трейдеров это открывает вопросы, на которые RQA для одного ряда ответить не может. Проходят ли пары EURUSD и GBPUSD через схожие динамические состояния? Ослабла ли взаимосвязь между золотом и долларом? Синхронизирована ли траектория индикатора в фазовом пространстве с траекторией цены, которую он, как предполагается, отслеживает? CRQA предоставляет методически обоснованную нелинейную основу для ответа на эти вопросы.

В данной статье библиотека RQA дополняется четырьмя новыми модулями: CCRQAMatrix, CCRQAMetrics, CCRQAWindow и фасадом CCRQA. Модуль скользящего окна использует OpenCL для параллельного пакетного вычисления матриц перекрёстной рекуррентности. Если GPU недоступен, используется CPU-реализация. Кроме того, мы предоставляем индикатор, который в режиме реального времени отображает метрики перекрёстной рекуррентности между двумя торговыми инструментами. Он включает выравнивание по временным меткам и встроенную нормализацию. Структура библиотеки повторяет то, что мы создали в предыдущей статье, поэтому, если вы следили за той реализацией, здешние паттерны будут вам знакомы. Однако математическая часть имеет важные отличия, которые мы подробно рассмотрим.


CRQA на графике

На изображении ниже показан индикатор CRQA, сравнивающий два торговых инструмента на графике в режиме реального времени. В отдельном окне под ценовым графиком отображаются четыре метрики: доля перекрёстной рекуррентности (CRR), перекрёстная детерминированность (CDET), перекрёстная ламинарность (CLAM) и перекрёстная энтропия (CENTR). Каждая из них вычисляется в скользящем окне.

CRQA на графике EURUSD

Рис. 1. Индикатор CRQA в отдельном окне, отображающий метрики CRR, CDET, CLAM и CENTR для двух торговых инструментов в режиме реального времени

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

Именно это и является результатом работы библиотеки. В оставшейся части этой статьи рассказывается, как она работает и как устроена.


От RQA к перекрёстной рекуррентности

Стандартный RQA сравнивает один временной ряд с самим собой. Матрица рекуррентности R является квадратной и симметричной: R(i, j) = R(j, i), поскольку расстояние от состояния i до состояния j равно расстоянию от j до i. Главная диагональ всегда заполнена единицами, поскольку каждое состояние совпадает с самим собой. Диагональные линии в верхнем и нижнем треугольниках зеркально отражают друг друга.

Перекрёстная рекуррентность меняет всё это. Теперь у нас есть два временных ряда, X и Y, длины которых могут быть разными. Каждый из них мы отдельно вкладываем в фазовое пространство, используя одинаковые размерность вложения и задержку. Затем мы вычисляем расстояние между каждым вектором из X и каждым вектором из Y. В результате получается матрица размером N × M, где N — количество векторов вложения из X, а M — количество из Y.

Сравнение матриц RQA и CRQA

Рис. 2. Стандартный метод RQA даёт квадратную симметричную матрицу (слева); метод CRQA — прямоугольную несимметричную матрицу размером N × M (справа)

Эта матрица не является симметричной. CR(i, j) задает вопрос: близко ли состояние X в момент времени i к состоянию Y в момент времени j? Нет никаких оснований полагать, что это должно равняться CR(j, i), которое задаёт обратный вопрос. Главной диагонали гарантированных единиц также нет, поскольку нет гарантии, что X и Y находятся в одном и том же состоянии одновременно. На самом деле самые интересные случаи — это те, когда это не так.

Определения метрик также меняются. В стандартном RQA при вычислении доли рекуррентности главную диагональ не учитывают (поскольку саморекуррентность тривиальна). В CRQA нет саморекуррентности, которую нужно исключать. Каждая ячейка матрицы размером N × M представляет собой допустимое сравнение, поэтому долю перекрёстной рекуррентности получают делением общего количества рекуррентных точек на N × M, а не на N² − N.

Изменяется также диагональная структура. В квадратной матрице саморекуррентности диагонали идут по элементам вида (i, i+k) при фиксированном смещении k. В прямоугольной матрице перекрёстной рекуррентности диагонали остаются параллельными линии i = j, однако их смещения и длины зависят от N и M: смещения варьируются от −(N−1) до +(M−1), и каждая диагональ может иметь свою длину. Логика подсчёта должна правильно обрабатывать эту ситуацию.

Вертикальные линии концептуально работают по тому же принципу, но на сетке размером N × M вместо N × N. Вертикальная линия в столбце j означает, что несколько последовательных состояний X все находятся вблизи состояния Y в момент времени j. Это указывает на то, что X оказался вблизи состояния, которое Y посетил в определённый момент.


Матрица перекрёстной рекуррентности

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

Шаг 1: вложение обоих рядов

Для ряда X длины Lx и ряда Y длины Ly мы выполняем вложение каждого из них независимо, используя одинаковые размерность вложения m и задержку tau. В результате получается N = Lx - (m-1)*tau векторов вложения из X и M = Ly - (m-1)*tau векторов вложения из Y. Параметры вложения должны быть одинаковыми для обоих рядов. Использование разных размерностей или задержек лишило бы вычисление расстояний смысла, поскольку при этом пришлось бы сравнивать векторы из пространств разной размерности.

Шаг 2: вычисление попарных расстояний

Для каждой пары (i, j), где i является индексом вектора X, а j — индексом вектора Y, мы вычисляем расстояние с использованием выбранной нормы (евклидовой, максимальной или манхэттенской). В результате получается матрица расстояний размером N × M. Когда N = M, она выглядит как стандартная матрица расстояний, но без гарантированно нулевой диагонали. Если N и M различаются, матрица является прямоугольной.

Шаг 3: применение порога

CR(i, j) = 1, если distance(x_i, y_j) <= epsilon, иначе 0

Порог epsilon определяет, что считается «достаточно близким», чтобы два состояния из разных рядов считались рекуррентными. Здесь используется та же концепция, что и в стандартном RQA, однако выбор порога epsilon становится более тонким, когда эти два ряда имеют разные масштабы. Мы рассмотрим этот вопрос ниже.

Пример диаграммы перекрёстной рекуррентности

Рис. 3. Диаграмма перекрёстной рекуррентности, показывающая бинарную матрицу размера N × M между двумя финансовыми временными рядами; диагональные структуры указывают на общую динамику


Метрики CRQA

В CRQA рассчитываются десять метрик. Основные понятия остаются такими же, как и в стандартном RQA (диагональные линии отражают степень детерминированности, а вертикальные — ламинарность), однако формулы адаптированы для неквадратной и несимметричной матрицы перекрёстной рекуррентности. В CRQA отсутствует метрика TREND, поскольку вычисление тренда основано на симметричной диагональной структуре матрицы саморекуррентности. Также отсутствует составной показатель COMPLEXITY.

Метрика
Символ
Формула
Что измеряет
Доля перекрёстной рекуррентности
CRR
(число рекуррентных точек) / (N * M)
Как часто состояния из X близки к состояниям из Y. Более высокое значение CRR означает большую общую область в пространстве состояний.
Перекрёстная детерминированность
CDET
(точки на диагональных линиях длиной &gt;= lmin) / (все рекуррентные точки)
Доля перекрёстных рекуррентных точек, образующих диагональные линии. Высокое значение CDET означает, что оба ряда проходят через схожие последовательности состояний.
Перекрёстная ламинарность
CLAM
(точки на вертикальных линиях длиной >= vmin) / (все рекуррентные точки)
Доля перекрёстных рекуррентных точек, образующих вертикальные линии. Высокое значение CLAM означает, что X оказывается захваченным вблизи состояний, которые посещал Y.
Перекрёстное время захвата
CTT
Средняя длина вертикальных линий
Средняя продолжительность, в течение которой X находится вблизи состояния Y.
Средняя длина перекрёстной диагонали
CL
Средняя длина диагональных линий длиной не менее lmin
Средняя продолжительность синхронной эволюции двух рядов.
Максимальная длина перекрёстной диагонали
CLmax
Самая длинная диагональная линия
Самый длительный участок, на котором оба ряда развивались параллельно.
Максимальная длина вертикальной линии
CVmax
Самая длинная вертикальная линия
Наибольший период, в течение которого X удерживался вблизи одного состояния Y.
Перекрёстная энтропия
CENTR
-sum(p(l) * ln(p(l)))
Сложность распределения перекрёстных диагональных линий.
Перекрёстная дивергенция
CDIV
1 / CLmax
Обратная величина длины самой длинной перекрёстной диагонали. Как быстро распадаются синхронизированные сегменты.
Перекрёстное отношение
CRATIO
CDET / CRR
Детерминированность, нормированная на долю рекуррентности. Устраняет влияние общей плотности перекрёстной рекуррентности.

Наиболее важные метрики для понимания взаимосвязей между рядами — CRR, CDET и CLAM.

CRR показывает, насколько два ряда перекрываются в пространстве состояний. Два инструмента, которые торгуются в одном диапазоне и проходят через схожие ценовые уровни, будут иметь высокий показатель CRR. Два инструмента с разными масштабами (например, EURUSD на уровне 1,08 и USDJPY на уровне 155) будут иметь CRR, близкий к нулю, если предварительно не нормализовать данные или тщательно не выбрать порог epsilon.

CDET позволяет заглянуть глубже. Высокое значение CRR при низком значении CDET означает, что оба ряда посещают схожие состояния, но не в схожих последовательностях. Они пересекаются в пространстве, но не во времени. Высокие значения CRR и CDET означают, что они синхронизированы: они не только находятся в схожих состояниях, но и переходят через эти состояния в одном и том же порядке и с близкими скоростями. Это самая сильная форма связи.

CLAM фиксирует асимметричный захват. Высокое значение CLAM означает, что ряд X часто задерживается вблизи состояний, через которые проходит ряд Y. С точки зрения торговли это может означать, что один инструмент консолидируется, в то время как другой движется по тренду в пределах той же ценовой зоны.


Эпсилон для двух рядов

В CRQA выбор порога epsilon представляет собой более сложную задачу, чем в стандартном RQA. При анализе одного временного ряда данные имеют один масштаб и одно распределение. При сравнении двух рядов вы можете столкнуться с тем, что они имеют совершенно разные масштабы. EURUSD торгуется около 1,08, а GBPUSD — около 1,27. Необработанное расстояние между их ценами закрытия определяется главным образом различием уровней, а не той динамикой, которая вас интересует.

Существует несколько подходов к решению этой задачи, и библиотека поддерживает их все:

  • Нормализуйте оба ряда перед вычислением CRQA. Индикатор поддерживает два режима нормализации. Z-нормализация заключается в вычитании среднего значения и делении на стандартное отклонение, что приводит оба ряда к масштабу с нулевым средним и единичной дисперсией. Нормализация на основе логарифмических доходностей преобразует цены в логарифмические доходности (которые по своей природе не зависят от масштаба), а затем дополнительно применяет z-нормализацию. В любом случае порог epsilon становится интерпретируемым в единицах стандартного отклонения: epsilon, равный 0,5, означает «в пределах половины стандартного отклонения».
  • Используйте исходные цены с тщательно подобранным порогом epsilon. При сравнении двух инструментов с одинаковой валютой котировки и схожими ценовыми уровнями (например, двух EUR-кроссов) можно пропустить нормализацию и использовать фиксированный порог epsilon, масштабированный с учётом типичного размера тика этих инструментов. При этом сохраняется информация об абсолютном уровне, но требуется ручная настройка.
  • Определите порог epsilon эмпирически. Проведите несколько тестовых вычислений и посмотрите, как изменяется CRR в зависимости от порога epsilon. Разумная отправная точка для нормализованных данных — от 0,3 до 0,8 (в единицах стандартного отклонения). Для валютных пар в исходных ценах с одинаковой валютой котировки типичен диапазон от 0,0005 до 0,001.

Библиотека принимает epsilon как фиксированный входной параметр для CRQA. Она не предусматривает автоматический выбор порога epsilon для случая перекрёстной рекуррентности (в отличие от стандартного модуля RQA, который предлагает поиск методом бисекции с ориентацией на заданное значение RR). Дело в том, что «правильное» целевое значение CRR в значительной степени зависит от того, что представляют собой эти два ряда, как они нормализованы и что именно вы ищете.

В индикаторе в конфигурации по умолчанию используется нормализация логарифмических доходностей с последующей z-нормализацией и порогом epsilon, равным 0,5. Это означает: два состояния считаются рекуррентными, если их нормированное расстояние находится в пределах половины стандартного отклонения. Такой подход хорошо работает с различными валютными парами без необходимости ручной настройки. Для сравнения исходных цен (например, двух валютных пар с одинаковой валютой котировки без нормализации) разумной отправной точкой является порог epsilon, равный 0,0005.


Архитектура библиотеки

Расширение CRQA добавляет в библиотеку четыре новых компонента, а также зависимость от OpenCL для вычислений в оконном режиме с ускорением на GPU. По структуре они повторяют стандартные модули RQA, но адаптированы для случая двух рядов. Все модули CRQA включаются автоматически при подключении файла RQA.mqh.

Архитектура библиотеки CRQA

Рис. 4. Архитектура библиотеки CRQA: CCRQAMatrix на нижнем уровне передаёт данные в CCRQAMetrics, который, в свою очередь, передаёт результаты в CCRQAWindow (с ускорением OpenCL) и фасад CCRQA на верхнем уровне

Файл
Класс
Назначение
CRQAMatrix.mqh
CCRQAMatrix
Независимо выполняет вложение двух рядов, вычисляет попарные расстояния между ними и строит бинарную матрицу перекрёстной рекуррентности размером N × M.
CRQAMetrics.mqh
CCRQAMetrics
Подсчитывает количество диагональных и вертикальных линий в неквадратной матрице. Вычисляет все десять метрик CRQA и заполняет структуру SCRQAResult.
CRQAWindow.mqh
CCRQAWindow
Применяет CRQA в скользящем окне к двум параллельным рядам. Использует ускорение на GPU через OpenCL с пакетным выполнением ядер, переходя к резервной объединённой CPU-реализации, если GPU недоступен. Содержит статические экстракторы для отдельных метрик.
RQA.mqh
CCRQA
Фасад высокого уровня. Настраивает и связывает CCRQAMatrix и CCRQAMetrics в один вызов Compute(), принимающий два ряда.

Фасад CCRQA и CCRQAWindow определены в соответствующих файлах. Все стандартные классы RQA (CRQAMatrix, CRQAMetrics, CRQAEpsilon, CRQAWindow, CRQA) остаются без изменений и полностью обратно совместимы. Единственная строка include остаётся прежней:

#include <RQA\RQA.mqh>

Теперь у вас есть доступ как к стандартным классам RQA, так и к перекрёстным классам RQA. Схема именования единообразна: стандартные классы используют префикс C + RQA (CRQAMatrix), а перекрёстные классы — префикс CC + RQA (CCRQAMatrix). Стандартные структуры результатов начинаются с S + RQA, а перекрёстные структуры результатов — с S + CRQA.

Модуль CRQAWindow использует встроенную поддержку OpenCL в MQL5 через стандартный класс-обёртку COpenCL из OpenCL/OpenCL.mqh. Это входит в стандартную библиотеку MetaTrader 5 и не требует установки дополнительных компонентов. Если в системе пользователя нет графического процессора с поддержкой OpenCL (или если среда выполнения OpenCL недоступна), модуль автоматически переключается на вариант вычислений только на CPU.


CCRQAMatrix: построение матрицы перекрёстной рекуррентности

CCRQAMatrix — это аналог CRQAMatrix для перекрёстной рекуррентности. Он принимает два ряда, вкладывает каждый из них в фазовое пространство и строит бинарную матрицу размером N × M, сравнивая каждый X-вектор с каждым Y-вектором.

Структура класса

Класс хранит отдельные массивы векторов вложения для каждого ряда (m_embX и m_embY) и отдельные счётчики размера (m_N для X, m_M для Y). Булева матрица m_R уплощается в порядке по строкам с шагом m_M: элемент (i, j) сопоставляется индексу i * m_M + j.

//+------------------------------------------------------------------+
//| CCRQAMatrix — NxM cross-recurrence matrix for two series         |
//|                                                                  |
//|  R[i,j] = 1  iff  ||x_i - y_j|| <= epsilon                       |
//|  where x_i = embedded vector from series X at time i             |
//|        y_j = embedded vector from series Y at time j             |
//+------------------------------------------------------------------+

class CCRQAMatrix
  {
private:
   int               m_N;          // embedded vectors from series X
   int               m_M;          // embedded vectors from series Y
   int               m_embDim;     // shared embedding dimension
   int               m_delay;      // shared time delay (tau)
   double            m_epsilon;    // threshold
   ENUM_RQA_NORM     m_norm;       // distance norm

   bool              m_R[];        // flattened N x M boolean matrix
   double            m_embX[];     // embedded X  [N x embDim]
   double            m_embY[];     // embedded Y  [M x embDim]

   //--- helpers
   void              Embed(const double &series[], int seriesLen,
                           double &embedded[], int &numVec);
   double            Distance(int i, int j) const;

public:
                     CCRQAMatrix();
                    ~CCRQAMatrix() {}

   //--- Build N x M cross-recurrence matrix
   bool              Build(const double &seriesX[], int lenX,
                           const double &seriesY[], int lenY,
                           double epsilon,
                           int embDim         = 1,
                           int delay          = 1,
                           ENUM_RQA_NORM norm = RQA_NORM_EUCLIDEAN);

   //--- Accessors
   bool              Get(int i, int j) const;
   int               SizeN()   const { return m_N; }      // rows (X)
   int               SizeM()   const { return m_M; }      // cols (Y)
   double            Epsilon() const { return m_epsilon; }
   int               EmbDim()  const { return m_embDim; }
   int               Delay()   const { return m_delay; }
   ENUM_RQA_NORM     Norm()    const { return m_norm; }
  };

Вложение двух рядов

Метод Embed() повторно используется для обоих рядов. Он принимает любой ряд и создаёт уплощённые векторы вложения. Этот же метод вызывается дважды: один раз для X и один раз для Y.

//+------------------------------------------------------------------+
//| Embed a single series into delay-coordinate vectors              |
//+------------------------------------------------------------------+
void CCRQAMatrix::Embed(const double &series[], int seriesLen,
                       double &embedded[], int &numVec)
  {
   numVec = seriesLen - (m_embDim - 1) * m_delay;
   if(numVec <= 0) { numVec = 0; return; }
   ArrayResize(embedded, numVec * m_embDim);
   for(int i = 0; i < numVec; i++)
      for(int d = 0; d < m_embDim; d++)
         embedded[i * m_embDim + d] = series[i + d * m_delay];
  }

Сравните это с методом CRQAMatrix::Embed() из предыдущей статьи. Логика остаётся той же, но сигнатура отличается: здесь выходной массив вложения и количество векторов передаются по ссылке в качестве параметров, а не хранятся непосредственно как члены класса. Это связано с тем, что классу CCRQAMatrix нужно выполнить вложение двух рядов в два отдельных массива (m_embX и m_embY), поэтому метод должен быть универсальным.

Вычисление перекрёстных расстояний

Метод Distance() вычисляет расстояние между вектором i из X и вектором j из Y. Он считывает данные из m_embX для первого операнда и из m_embY для второго.

//+------------------------------------------------------------------+
//| Distance between x_i and y_j                                     |
//+------------------------------------------------------------------+
double CCRQAMatrix::Distance(int i, int j) const
  {
   double dist = 0.0;
   for(int d = 0; d < m_embDim; d++)
     {
      double diff = m_embX[i * m_embDim + d]
                 - m_embY[j * m_embDim + d];
      switch(m_norm)
        {
         case RQA_NORM_MAX:
            dist = MathMax(dist, MathAbs(diff));
            break;
         case RQA_NORM_MANHATTAN:
            dist += MathAbs(diff);
            break;
         case RQA_NORM_EUCLIDEAN:
         default:
            dist += diff * diff;
            break;
        }
     }
   if(m_norm == RQA_NORM_EUCLIDEAN)
      dist = MathSqrt(dist);
   return dist;
  }

В этом заключается основное структурное отличие от стандартного вычисления расстояний в RQA. В методе CRQAMatrix::Distance() оба операнда обращаются к одному и тому же массиву m_embedded. Здесь первый операнд обращается к m_embX, а второй — к m_embY. Сама логика вычисления нормы осталась неизменной.

Построение матрицы

//+------------------------------------------------------------------+
//| Build the full N x M cross-recurrence matrix                     |
//+------------------------------------------------------------------+
bool CCRQAMatrix::Build(const double &seriesX[], int lenX,
                       const double &seriesY[], int lenY,
                       double epsilon,
                       int embDim,
                       int delay,
                       ENUM_RQA_NORM norm)
  {
   if(lenX < 2 || lenY < 2 || epsilon <= 0.0
     || embDim < 1 || delay < 1)
     {
      Print("CCRQAMatrix::Build - invalid parameters");
      return false;
     }
   m_epsilon = epsilon;
   m_embDim  = embDim;
   m_delay   = delay;
   m_norm    = norm;
   //--- Embed both series
   Embed(seriesX, lenX, m_embX, m_N);
   Embed(seriesY, lenY, m_embY, m_M);
   if(m_N <= 0 || m_M <= 0)
     {
      Print("CCRQAMatrix::Build - series too short");
      return false;
     }
   //--- Fill N x M boolean matrix
   ArrayResize(m_R, m_N * m_M);
   for(int i = 0; i < m_N; i++)
      for(int j = 0; j < m_M; j++)
         m_R[i * m_M + j] = (Distance(i, j) <= m_epsilon);
   return true;
  }

Метод Build() проверяет длины обоих рядов, выполняет вложение обоих рядов и заполняет булеву матрицу. Шаг матрицы равен m_M (числу векторов Y), а не m_N. Это объясняется тем, что каждая строка соответствует одному вектору X, а каждый столбец — одному вектору Y. Общий размер матрицы составляет m_N * m_M ячеек. Для двух 100-баровых рядов с размерностью вложения 1 это даёт 10 000 вызовов вычисления расстояния — те же затраты, что и в стандартном случае RQA. Для двух рядов разной длины, скажем 80 и 120, затраты составят 80 × 120 = 9 600.


CCRQAMetrics: количественная оценка перекрёстной структуры

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

Подсчёт диагоналей в неквадратной матрице

В квадратной матрице размером N × N диагонали индексируются смещением k от −(N−1) до +(N−1), и длина каждой диагонали равна N − |k|. В матрице размером N × M смещение k = j − i принимает значения от −(N−1) до +(M−1), а длина каждой диагонали зависит как от N, так и от M.

//+------------------------------------------------------------------+
//| Count diagonal lines in an NxM (possibly non-square) matrix      |
//|  Diagonals run parallel to the main diagonal (j - i = k)         |
//|  k ranges from -(N-1) to +(M-1)                                  |
//+------------------------------------------------------------------+
void CCRQAMetrics::CountDiagonals(const CCRQAMatrix &mat,
                                 int &lineLengths[]) const
  {
   int N = mat.SizeN();
   int M = mat.SizeM();
   int maxLen = MathMax(N, M);
   ArrayResize(lineLengths, maxLen + 1);
   ArrayInitialize(lineLengths, 0);
   //--- k = j - i, offset ranges from -(N-1) to +(M-1)
   for(int k = -(N - 1); k <= (M - 1); k++)
     {
      int len = 0;
      int iStart = MathMax(0, -k);
      int iEnd   = MathMin(N - 1, M - 1 - k);
      for(int i = iStart; i <= iEnd; i++)
        {
         int j = i + k;
         if(mat.Get(i, j))
            len++;
         else
           {
            if(len >= m_minDiagLine)
               lineLengths[MathMin(len, maxLen)]++;
            len = 0;
           }
        }
      if(len >= m_minDiagLine)
         lineLengths[MathMin(len, maxLen)]++;
     }
  }

Есть два важных отличия от стандартного счётчика диагоналей RQA. Во-первых, диапазон смещения асимметричен: он идёт от −(N−1) до +(M−1), а не от −(N−1) до +(N−1). Когда N и M различаются, с одной стороны диагоналей больше, чем с другой. Во-вторых, условия «пропустить k=0» нет. В стандартной RQA главная диагональ (k=0) исключается, поскольку она представляет собой тривиальную саморекуррентность. В CRQA диагональ при k=0 сравнивает x_i с y_i, и в этом нет ничего тривиального. То, что два разных ряда одновременно находятся в одном и том же состоянии, является содержательным результатом. Длина линии ограничивается значением maxLen = max(N, M), а при записи в гистограмму используется MathMin, чтобы предотвратить переполнение массива.

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

//+------------------------------------------------------------------+
//| Count vertical lines (fixed column j, vary row i)                |
//+------------------------------------------------------------------+
void CCRQAMetrics::CountVerticals(const CCRQAMatrix &mat,
                                 int &lineLengths[]) const
  {
   int N = mat.SizeN();
   int M = mat.SizeM();
   ArrayResize(lineLengths, N + 1);
   ArrayInitialize(lineLengths, 0);
   for(int j = 0; j < M; j++)
     {
      int len = 0;
      for(int i = 0; i < N; i++)
        {
         if(mat.Get(i, j))
            len++;
         else
           {
            if(len >= m_minVertLine)
               lineLengths[MathMin(len, N)]++;
            len = 0;
           }
        }
      if(len >= m_minVertLine)
         lineLengths[MathMin(len, N)]++;
     }
  }

При подсчёте вертикальных линий просматривается каждый столбец j (с проходом по всем строкам i). Внешний цикл проходит по M столбцам (Y-индексам), а внутренний цикл — по N строкам (X-индексам). Максимально возможная длина вертикальной линии равна N, поэтому размер гистограммы задаётся как N + 1. Вертикальная линия в столбце j означает, что несколько последовательных состояний X были близки к одному и тому же состоянию Y в момент времени j.

Метод Compute

//+------------------------------------------------------------------+
//| Main computation — fills SCRQAResult                             |
//+------------------------------------------------------------------+
bool CCRQAMetrics::Compute(const CCRQAMatrix &mat,
                          SCRQAResult &result) const
  {
   result.Reset();
   int N = mat.SizeN();
   int M = mat.SizeM();
   if(N < 2 || M < 2) return false;
   //--- 1. Cross Recurrence Rate
   long recCount = 0;
   long total    = (long)N * M;
   for(int i = 0; i < N; i++)
      for(int j = 0; j < M; j++)
         if(mat.Get(i, j))
            recCount++;
   result.CRR = (total > 0)
               ? (double)recCount / total : 0.0;
   //--- 2. Diagonal metrics
   int diagLengths[];
   CountDiagonals(mat, diagLengths);
   long  diagPoints = 0, totalDiagLines = 0;
   int   lmax = 0;
   int   sz = ArraySize(diagLengths);
   for(int l = m_minDiagLine; l < sz; l++)
     {
      if(diagLengths[l] > 0)
        {
         diagPoints     += (long)l * diagLengths[l];
         totalDiagLines += diagLengths[l];
         if(l > lmax) lmax = l;
        }
     }
   result.CLmax = (double)lmax;
   result.CDIV  = (lmax > 0) ? 1.0 / lmax : 0.0;
   result.CDET  = (recCount > 0)
                 ? (double)diagPoints / recCount : 0.0;
   result.CL    = (totalDiagLines > 0)
                 ? (double)diagPoints / totalDiagLines : 0.0;
   result.CENTR = ShannonEntropy(diagLengths,
                               (int)totalDiagLines);
   //--- 3. Vertical metrics
   int vertLengths[];
   CountVerticals(mat, vertLengths);
   long vertPoints = 0, totalVertLines = 0;
   int  vmax = 0;
   int  vsz = ArraySize(vertLengths);
   for(int l = m_minVertLine; l < vsz; l++)
     {
      if(vertLengths[l] > 0)
        {
         vertPoints     += (long)l * vertLengths[l];
         totalVertLines += vertLengths[l];
         if(l > vmax) vmax = l;
        }
     }
   result.CVmax = (double)vmax;
   result.CLAM  = (recCount > 0)
                 ? (double)vertPoints / recCount : 0.0;
   result.CTT   = (totalVertLines > 0)
                 ? (double)vertPoints / totalVertLines : 0.0;
   //--- 4. Derived
   result.CRATIO = (result.CRR > 1e-12)
                  ? result.CDET / result.CRR : 0.0;
   return true;
  }

Знаменатель CRR равен N * M (все ячейки), а не N² − N. Здесь нет главной диагонали, которую нужно исключать. Остальная логика следует той же схеме, что и стандартный метод Compute() в RQA: сначала по гистограммам накапливается статистика диагональных и вертикальных линий, а затем вычисляются совокупные метрики.


Ускорение на GPU с использованием OpenCL

CRQA в скользящем окне требует значительных вычислительных ресурсов. Для окна размером W с размерностью вложения, равной 1, каждое окно формирует матрицу размером N × N (где N = W), что требует N² сравнений расстояний. При наличии сотен или тысяч перекрывающихся окон общая стоимость вычислений составляет numWindows * N^2 операций. Для ряда из 500 баров с размером окна 50 и шагом 1 — то есть 451 окно по 2 500 сравнений в каждом — получается более миллиона вычислений расстояний. Параллелизм GPU естественно подходит для этой нагрузки, поскольку каждая ячейка в матрице рекуррентности независима: расстояние между векторами i и j не зависит ни от какой другой ячейки.

MQL5 обеспечивает полноценную поддержку OpenCL через класс-обёртку COpenCL из стандартной библиотеки. Это даёт нам доступ к любому GPU с поддержкой OpenCL (или даже к многоядерным CPU-бэкендам) без выхода из среды MQL5. Модуль CCRQAWindow использует это, чтобы передать вычисление расстояний и пороговую обработку на GPU, при этом оставляя подсчёт линий (диагональное и вертикальное сканирование) на CPU, где последовательная логика выглядит более естественной.

Ядро OpenCL

Ядро встроено в CRQAWindow.mqh как строковая константа. Оно вычисляет одну ячейку матрицы перекрёстной рекуррентности на каждый рабочий элемент, используя трёхмерное глобальное рабочее пространство: измерения (i, j, w), где i и j индексируют ячейку матрицы, а w — окно внутри пакета.

//+------------------------------------------------------------------+
//| OpenCL kernel source — embedded as string constant               |
//+------------------------------------------------------------------+
const string cl_crqa_source =
   "__kernel void crqa_recurrence(                              \r\n"
   "   __global const float *seriesX,                           \r\n"
   "   __global const float *seriesY,                           \r\n"
   "   __global int         *outR,                              \r\n"
   "   const int             N,                                 \r\n"
   "   const int             embDim,                            \r\n"
   "   const int             tau,                               \r\n"
   "   const int             norm,                              \r\n"
   "   const float           epsilon,                           \r\n"
   "   const int             step,                              \r\n"
   "   const int             baseWin)                           \r\n"
   "{                                                           \r\n"
   "   int i = get_global_id(0);                                \r\n"
   "   int j = get_global_id(1);                                \r\n"
   "   int w = get_global_id(2);                                \r\n"
   "   if(i >= N || j >= N) return;                             \r\n"
   "   int winStart = (baseWin + w) * step;                     \r\n"
   "   float dist = 0.0f;                                       \r\n"
   "   if(embDim == 1) {                                        \r\n"
   "      float diff = seriesX[winStart + i]                    \r\n"
   "                 - seriesY[winStart + j];                   \r\n"
   "      dist = (norm == 1) ? diff * diff : fabs(diff);        \r\n"
   "   } else {                                                 \r\n"
   "      for(int d = 0; d < embDim; d++) {                     \r\n"
   "         float diff = seriesX[winStart + i + d * tau]        \r\n"
   "                    - seriesY[winStart + j + d * tau];      \r\n"
   "         if(norm == 0)      dist = fmax(dist, fabs(diff));  \r\n"
   "         else if(norm == 1) dist += diff * diff;            \r\n"
   "         else               dist += fabs(diff);             \r\n"
   "      }                                                     \r\n"
   "   }                                                        \r\n"
   "   float threshold = (norm == 1)                            \r\n"
   "                   ? epsilon * epsilon : epsilon;           \r\n"
   "   outR[(w * N + i) * N + j] = (dist <= threshold) ? 1 : 0;\r\n"
   "}                                                           \r\n";

Стоит отметить несколько проектных решений, реализованных в этом ядре. Во-первых, ядро обрабатывает все три нормы расстояния с помощью целочисленного параметра norm: 0 — для нормы максимума (Чебышёва), 1 — для евклидовой и 2 — для манхэттенской. Эти значения соответствуют перечислению ENUM_RQA_NORM. Во-вторых, для евклидовой нормы ядро сравнивает квадрат расстояния с квадратом epsilon (в строке вычисления порога рассчитывается epsilon * epsilon, когда norm == 1). Это позволяет избежать дорогостоящего вычисления квадратного корня во внутреннем цикле каждого рабочего элемента. В-третьих, в ядре предусмотрен быстрый вариант для случая embDim == 1, который полностью пропускает цикл вложения, поскольку это наиболее распространённая конфигурация для финансовых данных. В-четвёртых, формула выходного индекса (w * N + i) * N + j размещает матрицы всех окон подряд: окно 0 занимает индексы от 0 до N²-1, окно 1 — от N² до 2*N²-1 и так далее.

Пакетное выполнение

Функция RunGPU() обрабатывает окна пакетами. Оба полных ряда (X и Y) загружаются на графический процессор один раз в виде буферов с числами типа float. Каждый пакет вычисляет несколько окон за один запуск ядра.

//--- Batch sizing: cap output buffer at ~64 MB
long cellsPerWin = (long)N * N;
int  maxBatch = (int)MathMin((long)numWindows,
                            64L * 1024 * 1024 / (cellsPerWin * (long)sizeof(int)));
if(maxBatch < 1) maxBatch = 1;

Размер пакета ограничивается так, чтобы выходной буфер (в котором хранится batchSize * N * N целых чисел) не превышал 64 мегабайта. Это позволяет избежать ошибок, связанных с нехваткой памяти, на графических процессорах с ограниченным объёмом видеопамяти, при этом обеспечивая максимальную пропускную способность. При типичном размере окна, равном 50 (N = 50 при embDim = 1), каждое окно генерирует 2 500 целых чисел (10 КБ), поэтому один пакет объёмом 64 МБ может содержать более 6 000 окон. На практике это означает, что большинство вычислений завершается за один запуск ядра.

Последовательность выполнения для каждого пакета следующая: выделить выходной буфер на GPU, задать аргумент baseWin, чтобы указать ядру, какой пакет окон нужно вычислить, запустить ядро с трёхмерным размером рабочей области (N, N, batchSize), считать целочисленные результаты, а затем просканировать на ЦП подмассив N × N каждого окна, чтобы извлечь метрики диагональных и вертикальных линий.

uint gOff[3]  = {0, 0, 0};
uint gWork[3] = {(uint)N, (uint)N, (uint)batchSize};
if(!ocl.Execute(0, 3, gOff, gWork))
  { ok = false; break; }

Сканирование метрик на ЦП

После того как графический процессор формирует булевы матрицы (в виде целочисленных массивов, состоящих из нулей и единиц), центральный процессор анализирует их на наличие линейных структур с помощью метода ScanMetrics(). Этот метод принимает плоский массив целых чисел, размерность матрицы N и базовый индекс в массиве, а затем вычисляет все десять метрик CRQA для данного окна. Логика аналогична функции CCRQAMetrics::Compute(), но работает с плоским массивом int[], а не с объектом CCRQAMatrix. Диагональное сканирование проходит по смещениям k в диапазоне от -(N-1) до +(N-1), а вертикальное сканирование — по столбцам. Оба прохода формируют гистограммы и на их основе вычисляют агрегированные метрики.

Зачем разделять GPU и CPU?

Вычисление расстояний и пороговая обработка (этапы 2 и 3 построения матрицы) тривиально распараллеливаются: каждая ячейка независима. Именно здесь графический процессор особенно эффективен. Однако подсчёт линий (диагональных и вертикальных) по своей сути выполняется последовательно вдоль каждой линии с учётом условного состояния (отслеживания длины текущей линии). Хотя реализовать это на графическом процессоре вполне возможно, дополнительная сложность и накладные расходы на синхронизацию делают такой подход нецелесообразным для матриц тех размеров, с которыми мы работаем (как правило, 50×50 или, в крайнем случае, несколько сотен на несколько сотен). Гибридный подход позволяет использовать преимущества обоих вариантов: графический процессор (GPU) выполняет основную работу сложности O(numWindows * N^2), а центральный процессор (CPU) — последовательные сканирования сложности O(numWindows * N).


CCRQAWindow: скользящий перекрёстный анализ

CCRQAWindow применяет CRQA в скользящем окне к двум параллельным рядам. Оба ряда разбиваются на окна с одинаковым размером окна и шагом. На каждом шаге метод берёт один и тот же диапазон из X и Y, строит матрицу перекрёстной рекуррентности для этой пары срезов и сохраняет метрики. Класс сначала пробует использовать графический процессор. Если инициализация OpenCL завершается сбоем (GPU недоступен, драйвер не установлен), система автоматически переключается на режим работы исключительно на CPU.

Точка входа Run()

//+------------------------------------------------------------------+
//| Main entry — tries GPU, falls back to CPU                        |
//+------------------------------------------------------------------+
bool CCRQAWindow::Run(const double &seriesX[], int lenX,
                     const double &seriesY[], int lenY,
                     SCRQAWindowResult &results[])
  {
   int minLen = MathMin(lenX, lenY);
   if(minLen < m_windowSize)
     {
      Print("CCRQAWindow::Run - series shorter than window");
      return false;
     }
   int numWindows = (minLen - m_windowSize) / m_step + 1;
   if(RunGPU(seriesX, seriesY, minLen, numWindows, results))
      return true;
   Print("CRQA: OpenCL unavailable, using CPU fallback");
   ArrayResize(results, numWindows);
   for(int idx = 0; idx < numWindows; idx++)
     {
      int start = idx * m_step;
      results[idx].barIndex = start;
      ComputeFusedCPU(seriesX, start,
                      seriesY, start,
                      results[idx].metrics);
     }
   return true;
  }

В данном методе в качестве эффективной длины используется MathMin(lenX, lenY). Перед передачей оба ряда должны быть выровнены по времени. Если в ряде X 500 баров, а в ряде Y — 480, то используются только первые 480 баров. Число окон вычисляется по формуле (minLen - windowSize) / step + 1.

Резервная объединённая CPU-реализация

Если графический процессор недоступен, функция ComputeFusedCPU() обрабатывает каждое окно. В отличие от наивного подхода, при котором сначала вызывается CCRQAMatrix::Build(), а затем — CCRQAMetrics::Compute(), данный метод объединяет построение матрицы и извлечение метрик в один проход. Он никогда не выделяет явную булеву матрицу. Вместо этого он вычисляет расстояние на месте и сразу обновляет диагональную и вертикальную гистограммы.

//+------------------------------------------------------------------+
//| CPU fallback — single window fused compute                       |
//+------------------------------------------------------------------+
void CCRQAWindow::ComputeFusedCPU(const double &sX[], int offX,
                                 const double &sY[], int offY,
                                 SCRQAResult &result)
  {
   result.Reset();
   int N = m_windowSize - (m_embDim - 1) * m_delay;
   if(N <= 1) return;
   long NM = (long)N * N;
   double epsSq = m_epsilon * m_epsilon;
   int diagHist[], vertHist[];
   ArrayResize(diagHist, N + 1);
   ArrayResize(vertHist, N + 1);
   ArrayInitialize(diagHist, 0);
   ArrayInitialize(vertHist, 0);
   long recCount = 0;
   //--- Fast path: embDim==1, Euclidean
   if(m_embDim == 1 && m_norm == RQA_NORM_EUCLIDEAN)
     {
      //--- Diagonal scan
      for(int k = -(N - 1); k <= (N - 1); k++)
        {
         int iS = (k < 0) ? -k : 0;
         int iE = (k < 0) ? N - 1 : N - 1 - k;
         int len = 0;
         for(int i = iS; i <= iE; i++)
           {
            double diff = sX[offX + i] - sY[offY + i + k];
            if(diff * diff <= epsSq)
              { len++; recCount++; }
            else
              { if(len >= m_minDiagLine) diagHist[len]++; len = 0; }
           }
         if(len >= m_minDiagLine) diagHist[len]++;
        }
      //--- Vertical scan
      for(int j = 0; j < N; j++)
        {
         int len = 0;
         double yj = sY[offY + j];
         for(int i = 0; i < N; i++)
           {
            double diff = sX[offX + i] - yj;
            if(diff * diff <= epsSq) len++;
            else { if(len >= m_minVertLine) vertHist[len]++; len = 0; }
           }
         if(len >= m_minVertLine) vertHist[len]++;
        }
     }
   //--- ... general case handles embDim > 1 and other norms
  }

В объединённом подходе реализованы две важные оптимизации. Во-первых, для евклидовой нормы используется сравнение с квадратом порога epsilon (diff * diff <= epsSq), что позволяет избежать вычислительно затратного вызова MathSqrt() для каждой ячейки. Во-вторых, в типичном случае, когда embDim == 1, алгоритм осуществляет прямой доступ к значениям ряда с использованием смещения вместо построения векторов вложения, что позволяет полностью устранить накладные расходы, связанные с вложением. Подсчёт рекуррентности выполняется во время диагонального сканирования, поэтому при вертикальном сканировании требуется обновлять только собственную гистограмму. После завершения обоих сканирований метод вычисляет все десять метрик на основе гистограмм таким же образом, как и CCRQAMetrics::Compute().

Статические экстракторы

Статические экстракторы построены по той же схеме, что и в стандартном оконном модуле RQA. Для каждой из широко используемых метрик предусмотрен свой экстрактор: ExtractCRR, ExtractCDET, ExtractCLAM, ExtractCTT, ExtractCENTR и ExtractCLmax. Каждый из них принимает массив результатов и формирует простой массив с элементами типа double для данной метрики.

void CCRQAWindow::ExtractCRR(const SCRQAWindowResult &r[],
                            double &out[])
  {
   int n = ArraySize(r);
   ArrayResize(out, n);
   for(int i = 0; i < n; i++)
      out[i] = r[i].metrics.CRR;
  }


CCRQA: Фасад

Фасад CCRQA объединяет CCRQAMatrix и CCRQAMetrics в один объект с единым вызовом Compute(). В отличие от фасада RQA, описанного в предыдущей статье, здесь отсутствует автоматический выбор порога epsilon. Порог epsilon задаётся напрямую с помощью функции SetEpsilon().

CCRQA crqa;
crqa.SetEpsilon(0.0005);
crqa.SetEmbedding(2, 1);
crqa.SetNorm(RQA_NORM_EUCLIDEAN);
if(crqa.Compute(closeX, lenX, closeY, lenY))
   crqa.PrintSummary();

Метод Compute() принимает два ряда с указанием их длин. Они могут различаться по длине. Внутри алгоритм формирует матрицу перекрёстной рекуррентности и вычисляет все десять метрик.

//+------------------------------------------------------------------+
//| Build cross-recurrence matrix and compute all CRQA metrics       |
//+------------------------------------------------------------------+
bool CCRQA::Compute(const double &seriesX[], int lenX,
                    const double &seriesY[], int lenY)
  {
   m_computed = false;
   m_result.Reset();
   if(lenX < 4 || lenY < 4)
     {
      Print("CCRQA::Compute - series too short");
      return false;
     }
   if(!m_matrix.Build(seriesX, lenX, seriesY, lenY,
                      m_epsilon, m_embDim, m_delay, m_norm))
      return false;
   if(!m_metrics.Compute(m_matrix, m_result))
      return false;
   m_computed = true;
   return true;
  }

После того как функция Compute() вернёт значение true, можно получить доступ к отдельным метрикам с помощью именованных методов доступа (CRR(), CDET(), CLAM() и т. д.) или извлечь полную структуру SCRQAResult с помощью метода GetResult(). Метод PrintSummary() выводит все десять метрик в журнал Experts, включая размеры матрицы и значение порога epsilon.


Индикатор CRQA

Файл CRQA_Indicator.mq5 превращает библиотеку в инструмент для сравнения в режиме реального времени. Индикатор сравнивает торговый инструмент текущего графика со вторым торговым инструментом, указанным пользователем, и отображает в отдельном окне четыре метрики: CRR, CDET, CLAM и CENTR. В отличие от наивной реализации, которая просто получала бы цены закрытия по обоим торговым инструментам и сравнивала их напрямую, этот индикатор реализует корректное выравнивание по временным меткам, встроенную нормализацию и инкрементный расчёт для надёжной работы в условиях реальной торговли.

Настройка буферов и графиков

#property indicator_separate_window
#property indicator_buffers 4
#property indicator_plots   4
#property indicator_label1  "CRR"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrDodgerBlue
#property indicator_width1  2
#property indicator_label2  "CDET"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrLimeGreen
#property indicator_width2  2
#property indicator_label3  "CLAM"
#property indicator_type3   DRAW_LINE
#property indicator_color3  clrOrange
#property indicator_width3  2
#property indicator_label4  "CENTR"
#property indicator_type4   DRAW_LINE
#property indicator_color4  clrViolet
#property indicator_width4  1

Четыре буфера, соответствующие цветовой схеме стандартного индикатора RQA для общих типов метрик. Индикатор CRQA не отображает линию TREND, поскольку CRQA не рассчитывает метрику тренда.

Входные параметры и нормализация

#include <RQA\RQA.mqh>

enum ENUM_CRQA_NORMALIZE
  {
   CRQA_NORM_NONE    = 0,   // None (raw prices)
   CRQA_NORM_ZSCORE  = 1,   // Z-Score (recommended for cross-symbol)
   CRQA_NORM_RETURNS = 2    // Log Returns
  };

input string InpSymbolY      = "GBPUSD";
input int    InpWindowSize   = 50;
input int    InpStep         = 1;
input int    InpEmbDim       = 1;
input int    InpDelay        = 1;
input double InpEpsilon      = 0.5;
input ENUM_RQA_NORM InpNorm  = RQA_NORM_EUCLIDEAN;
input int    InpMinDiag      = 2;
input int    InpMinVert      = 2;
input ENUM_CRQA_NORMALIZE InpNormalize = CRQA_NORM_RETURNS;

Перечисление нормализации определено локально в файле индикатора. Оно определяет, как выполняется предварительная обработка двух ценовых рядов перед вычислением CRQA. В режиме по умолчанию, CRQA_NORM_RETURNS, цены преобразуются в логарифмические доходности, а затем результат стандартизируется в z-оценки. Благодаря этому параметр epsilon можно интерпретировать в единицах стандартного отклонения независимо от уровней цен торговых инструментов. Порог epsilon, равный 0,5, означает «в пределах половины стандартного отклонения нормализованного ряда», что хорошо подходит для сравнения любых двух валютных пар или инструментов без ручной настройки.

Режим z-оценки (CRQA_NORM_ZSCORE) нормирует исходные цены, приводя их к среднему значению, равному нулю, и дисперсии, равной единице. Это полезно в тех случаях, когда вы хотите напрямую сравнивать динамику уровней (а не доходность), но при этом вам по-прежнему требуется независимость от масштаба. Режим «None» пропускает исходные цены закрытия без изменений, что имеет смысл только при сравнении инструментов со схожими уровнями цен.

Выравнивание по временным меткам

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

//+------------------------------------------------------------------+
//| Bulk-align second symbol's close prices to chart bars            |
//| Uses CopyTime + CopyClose in bulk, then merge-joins by datetime  |
//+------------------------------------------------------------------+
bool AlignPrices(const datetime &timeX[], const double &closeX[],
                 int rates_total)
  {
   datetime timeY[];
   double   closeY[];
   int copiedT = CopyTime(InpSymbolY, _Period, 0, rates_total, timeY);
   if(copiedT <= 0) return false;
   int copiedC = CopyClose(InpSymbolY, _Period, 0, rates_total, closeY);
   if(copiedC != copiedT) return false;
   //--- Merge-join both arrays by datetime
   int jj = 0;
   g_validCount = 0;
   for(int ix = 0; ix < rates_total; ix++)
     {
      while(jj < copiedT && timeY[jj] < timeX[ix])
         jj++;
      if(jj < copiedT && timeY[jj] == timeX[ix])
        {
         g_pricesX[g_validCount] = closeX[ix];
         g_pricesY[g_validCount] = closeY[jj];
         g_barMap[g_validCount]  = ix;
         g_validCount++;
        }
     }
   return g_validCount >= InpWindowSize;
  }

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

Этот подход имеет линейную сложность (один проход по каждому массиву) и обрабатывает все пограничные случаи: разные часы торгов, отсутствующие бары у любого из инструментов, а также символы с различной глубиной истории.

Конвейер нормализации

После выравнивания индикатор нормализует оба ряда в соответствии с параметром InpNormalize. Режим логарифмических доходностей (по умолчанию) реализует двухэтапный конвейер: сначала выровненные цены преобразуются в логарифмические доходности, а затем доходности нормализуются по z-оценке.

if(InpNormalize == CRQA_NORM_RETURNS)
  {
   int newLenX = ToLogReturns(g_pricesX, g_validCount);
   int newLenY = ToLogReturns(g_pricesY, g_validCount);
   //--- Shift barMap forward (returns[i] corresponds to bar[i+1])
   for(int i = 0; i < newLenX; i++)
      g_barMap[i] = g_barMap[i + 1];
   g_validCount = MathMin(newLenX, newLenY);
   //--- Z-score the returns for scale-invariant epsilon
   NormalizeZScore(g_pricesX, g_validCount);
   NormalizeZScore(g_pricesY, g_validCount);
  }

Логарифмические доходности полностью устраняют влияние уровня цен: MathLog(price[i+1] / price[i]) формирует ряд, центрированный около нуля, независимо от того, равна ли цена базового актива 1,08 или 155,0. Последующая нормализация по z-оценке гарантирует, что оба ряда доходностей имеют среднее значение 0 и стандартное отклонение 1. Благодаря этому параметр epsilon становится универсальным: epsilon, равный 0,5, означает «в пределах половины стандартного отклонения» для любой пары торговых инструментов на любом таймфрейме. Обратите внимание, что при преобразовании в доходности массив сокращается на один элемент, а barMap необходимо сдвинуть вперёд, поскольку return[i] соответствует бару в позиции i+1.

Логика функции OnCalculate

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

//+------------------------------------------------------------------+
//| Main calculation: align series, normalize, run CRQA windows      |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
               const int prev_calculated, ...)
  {
   if(rates_total < InpWindowSize + 10) return 0;
   if(prev_calculated == rates_total) return rates_total;
   bool fullRecalc = (prev_calculated == 0
                    || g_lastAligned != prev_calculated);
   if(fullRecalc)
     {
      //--- Full alignment + normalization + CRQA on all windows
      if(!AlignPrices(time, close, rates_total)) return 0;
      //--- Normalize, clear buffers, run CCRQAWindow
      CCRQAWindow win;
      win.SetWindow(InpWindowSize, InpStep);
      win.SetEpsilon(InpEpsilon);
      win.SetEmbedding(InpEmbDim, InpDelay);
      win.SetNorm(InpNorm);
      win.SetMinLines(InpMinDiag, InpMinVert);
      SCRQAWindowResult results[];
      win.Run(g_pricesX, g_validCount,
              g_pricesY, g_validCount, results);
      //--- Map results back to chart bars via g_barMap
      for(int k = 0; k < ArraySize(results); k++)
        {
         int lastValid = results[k].barIndex + InpWindowSize - 1;
         if(lastValid < g_validCount)
           {
            int bar = g_barMap[lastValid];
            BufferCRR[bar]   = results[k].metrics.CRR;
            BufferCDET[bar]  = results[k].metrics.CDET;
            BufferCLAM[bar]  = results[k].metrics.CLAM;
            BufferCENTR[bar] = results[k].metrics.CENTR;
           }
        }
     }
   else
     {
      //--- Incremental: realign, renormalize, compute only tail windows
     }
   return rates_total;
  }

Результаты присваиваются последнему бару каждого окна (barIndex + InpWindowSize - 1), а g_barMap переводит выровненный индекс обратно в фактическую позицию бара на графике. Это гарантирует, что значения индикатора будут отображаться на правильном баре, даже если при выравнивании некоторые бары были пропущены.

Инкрементальная ветвь (ветвь else) повторно выравнивает всю историю (поскольку нормализация зависит от глобальной статистики), но затем вычисляет только окна CRQA, охватывающие конечный участок данных. Это позволяет сократить вычислительную нагрузку, когда новые бары поступают по одному во время реальной торговли.

Диалоговое окно настроек индикатора CRQA

Рис. 5. Диалоговое окно настроек индикатора CRQA, в котором отображаются второй торговый инструмент, epsilon, размер окна, режим нормализации и другие входные параметры

CRQA: сравнение EURUSD и GBPUSD

Рис. 6. Индикатор CRQA, сравнивающий GBPUSD с EURUSD с использованием нормализации по логарифмическим доходностям; CRR, CDET, CLAM и CENTR отображаются в отдельном окне индикатора


Что дальше?

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

  • Использование CRQA в скользящем окне для коррелирующих пар (например, EURUSD и GBPUSD), чтобы определить, когда их связь нарушается, что может служить сигналом для сделок на расхождении.
  • Объединение стандартных метрик RQA для одного ряда с метриками CRQA, рассчитанными между этим рядом и эталонным рядом, для построения композитного индикатора режима.
  • Использование метрик RQA и CRQA в качестве признаков в модели машинного обучения для классификации режимов или генерации торговых сигналов.
  • Применение CRQA между ценой и индикатором (например, RSI или скользящим средним), чтобы определить, действительно ли индикатор синхронизирован с той динамикой, которую он, как предполагается, отслеживает.

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


Заключение

Количественный анализ перекрёстной рекуррентности расширяет RQA, превращая его из инструмента для одного ряда в основу для сравнения двух рядов. Сравнивая траектории двух временных рядов в фазовом пространстве, метод CRQA выявляет совместную динамику, синхронизацию и связь, которые не удаётся обнаружить с помощью стандартных мер корреляции. В данной статье представлена полная реализация на языке MQL5, состоящая из четырёх новых модулей:

  • CCRQAMatrix выполняет вложение двух рядов независимо друг от друга и строит бинарную матрицу перекрёстной рекуррентности размером N × M с использованием трёх норм расстояния.
  • CCRQAMetrics извлекает из матрицы перекрёстной рекуррентности десять метрик, охватывающих диагональную структуру (CDET, CL, CLmax, CENTR, CDIV), вертикальную структуру (CLAM, CTT, CVmax) и агрегированные метрики (CRR, CRATIO).
  • CCRQAWindow применяет CRQA в скользящем окне, используя ускоренное на GPU построение матрицы через OpenCL, с автоматической резервной CPU-реализацией для систем без поддержки GPU.
  • CCRQA предоставляет высокоуровневый фасад для наиболее распространённого сценария использования: вычисления всех метрик между двумя рядами за один вызов.

Данный индикатор в режиме реального времени строит графики метрик CRR, CDET, CLAM и CENTR между символом графика и любым вторым символом, с правильным выравниванием по временным меткам, встроенной нормализацией (логарифмические доходности с z-оценкой) и инкрементным вычислением для торговли в реальном времени.

Все модули обратно совместимы со стандартной библиотекой RQA из предыдущей статьи. Одного подключения RQA.mqh достаточно, чтобы получить доступ ко всему.

# Имя файла
Тип Описание
1 RQA.mqh
include-файл Обновлён основной include-файл с фасадами CRQA и CCRQA
2 RQAMatrix.mqh
include-файл Класс CRQAMatrix: стандартная матрица рекуррентности для одного ряда (без изменений)
3 RQAMetrics.mqh
include-файл Класс CRQAMetrics и структура SRQAResult (без изменений)
4 RQAEpsilon.mqh
include-файл Класс CRQAEpsilon: автоматический выбор порога epsilon (без изменений)
5 RQAWindow.mqh
include-файл Класс CRQAWindow: стандартный анализ в скользящем окне (без изменений)
6 CRQAMatrix.mqh
include-файл Класс CCRQAMatrix: построение матрицы перекрёстной рекуррентности размером N × M
7 CRQAMetrics.mqh
include-файл Класс CCRQAMetrics и структура SCRQAResult: все десять метрик CRQA
8 CRQAWindow.mqh
include-файл Класс CCRQAWindow: перекрёстная рекуррентность в скользящем окне с ускорением на GPU и резервной CPU-реализацией
9 RQA_Example.mq5
Скрипт Пример скрипта для стандартного RQA (из предыдущей статьи)
10 RQA_Indicator.mq5
Индикатор Стандартный индикатор RQA (из предыдущей статьи)
11 CRQA_Indicator.mq5
Индикатор Индикатор Cross-RQA с ускорением на GPU, выравниванием по временным меткам и нормализацией

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

Прикрепленные файлы |
RQA.mqh (13.04 KB)
RQAMatrix.mqh (5.62 KB)
RQAMetrics.mqh (8.83 KB)
RQAEpsilon.mqh (4.86 KB)
RQAWindow.mqh (5.81 KB)
CRQAMatrix.mqh (6.2 KB)
CRQAMetrics.mqh (8.65 KB)
CRQAWindow.mqh (18.67 KB)
CRQA_Indicator.mq5 (11.64 KB)
RQA_Example.mq5 (2.66 KB)
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
От начального к среднему уровню: Объекты и подокна (III) От начального к среднему уровню: Объекты и подокна (III)
В этой статье описывается безопасный способ обработки удаления графических объектов: перехват событий, воссоздание объекта и восстановление минимального набора свойств. Мы покажем, как избежать ошибок при работе с указателями, предотвратить нежелательное воссоздание объектов при закрытии и сохранять положение и размеры, обновляя их с помощью DRAG/CHANGE. Таким образом, индикатор остаётся целостным, а пользователь сохраняет часть своих настроек.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
От начального к среднему уровню: Объекты и подокна (II) От начального к среднему уровню: Объекты и подокна (II)
В этой статье объясняется, как отслеживать и обрабатывать удаление объектов с графика в MQL5 с использованием событий MetaTrader 5. При обнаружении удаления объекта, созданного индикатором, код удаляет соответствующий экземпляр, чтобы избежать несогласованностей и появления оставшихся подокон. В этом подходе используются уникальные имена и файл заголовка, при этом приоритет отдаётся простоте и предсказуемости поведения.