Количественный анализ совместной рекуррентности (JRQA) в MQL5: Выявление одновременной рекуррентности в двух временных рядах
Содержание
- Введение
- JRQA на графике
- От перекрестной рекуррентности к совместной рекуррентности
- Матрица совместной рекуррентности
- Метрики JRQA
- Эпсилон для совместной рекуррентности
- Архитектура библиотеки
- CJRQAMatrix: построение матрицы совместной рекуррентности
- CJRQAMetrics: количественная оценка совместной структуры
- CJRQAWindow: скользящий совместный анализ с ускорением на GPU
- CJRQA: фасад
- Индикатор JRQA
- Что дальше?
- Заключение
Введение
В первой статье этой серии была создана библиотека RQA, которая анализирует отдельный временной ряд относительно его собственного прошлого. Во второй статье этот подход был дополнен методом количественного анализа перекрестной рекуррентности (CRQA), который сравнивает траектории двух разных временных рядов в фазовом пространстве. CRQA отвечает на вопрос: если временной ряд X находится в состоянии x_i, оказывается ли временной ряд Y когда-либо в сходном состоянии y_j? Это полезный вопрос, но это не единственный вопрос, который можно задать о двух системах.
В данной статье предлагается третий подход: количественный анализ совместной рекуррентности (JRQA). Вместо того чтобы спрашивать, посещают ли X и Y схожие состояния, JRQA задает вопрос о том, возвращаются ли X и Y одновременно к своим собственным прошлым состояниям при одних и тех же временных индексах. Совместная рекуррентность в точке (i, j) означает, что состояние X в момент времени i близко к состоянию X в момент времени j и при этом состояние Y в момент времени i близко к состоянию Y в момент времени j. Обе системы должны проявлять рекуррентность для одной и той же пары временных точек. Ни того, ни другого по отдельности недостаточно.
Это различие имеет значение для торговли. CRQA обнаруживает общую область пространства состояний. JRQA выявляет синхронизированное режимное поведение. Два инструмента могут находиться на схожих ценовых уровнях (высокое значение CRQA), не находясь при этом одновременно в одном и том же динамическом режиме. И наоборот, два инструмента могут оставаться в синхронизированных режимах (высокое значение JRQA), торгуясь при этом в совершенно разных масштабах. JRQA фиксирует именно эту закономерность.
В данной статье добавлены три модуля (CJRQAMatrix, CJRQAMetrics, CJRQAWindow) и обновлен фасад CJRQA в файле RQA.mqh. Кроме того, в статье представлен индикатор, который в режиме реального времени отображает метрики совместной рекуррентности для двух символов. Модуль скользящего окна поддерживает ускорение на GPU через OpenCL с автоматическим переходом на CPU. Как и в предыдущих статьях, здесь не строится торговая стратегия. Цель — создать протестированный аналитический слой, который можно включать в эксперименты.
JRQA на графике
На изображении ниже показан индикатор JRQA, сравнивающий два символа (финансовых инструмента) на графике в режиме реального времени. В отдельном окне строятся графики пяти метрик: коэффициента совместной рекуррентности (JRR), совместного детерминизма (JDET), совместной ламинарности (JLAM), совместной энтропии (JENTR) и совместного тренда (JTREND). Каждая метрика вычисляется на скользящем окне двух выровненных и нормализованных временных рядов.

Рис. 1. Индикатор JRQA в отдельном окне, отображающий показатели JRR, JDET, JLAM, JENTR и JTREND для двух символов (финансовых инструментов) в режиме реального времени
Высокое значение JRR означает, что обе системы возвращаются к своим прошлым состояниям при одних и тех же индексах. Высокое значение JDET означает, что эти случаи совместной рекуррентности образуют диагональные линии, что свидетельствует о параллельной эволюции. Когда JLAM достигает пика, обе системы совместно оказываются захваченными в своих соответствующих состояниях. Рост показателя JTREND означает, что совместная рекуррентность становится более частой для близких временных индексов по сравнению с отдалёнными, что свидетельствует о смене режима связи между двумя системами.
Именно это и является результатом работы библиотеки. Далее в этой статье рассказывается, как это сделать.
От перекрестной рекуррентности к совместной рекуррентности
Методы CRQA и JRQA анализируют два временных ряда, однако они ставят принципиально разные вопросы и дают матрицы, отличающиеся по структуре.
В методе CRQA мы выполняем вложение X и Y в одно и то же фазовое пространство и сравниваем X-векторы с Y-векторами. Матрица перекрестной рекуррентности CR(i, j) проверяет: близко ли состояние X в момент времени i к состоянию Y в момент времени j? Матрица имеет размер N×M (и потенциально может быть прямоугольной), не является симметричной и не имеет гарантированной главной диагонали. Она характеризует общую область пространства состояний.
В JRQA мы строим две отдельные матрицы саморекуррентности и вычисляем их поэлементное логическое И. Матрица совместной рекуррентности JR(i, j) задает следующий вопрос: близко ли состояние X в момент времени i к состоянию X в момент времени j, и одновременно близко ли состояние Y в момент времени i к состоянию Y в момент времени j? Оба условия должны выполняться одновременно. Матрица имеет размер N×N (всегда квадратная), является симметричной и содержит главную диагональ, состоящую из единиц (каждое состояние в обеих системах совпадает с самим собой). Она измеряет синхронизированное динамическое поведение.

Рис. 2. CRQA сравнивает X с Y в одной матрице (слева); JRQA вычисляет две матрицы саморекуррентности и выполняет над ними операцию «И» (справа)
Формальное определение выглядит следующим образом:
JR(i, j) = Theta(epsilonX - ||x_i - x_j||) * Theta(epsilonY - ||y_i - y_j||)
где Theta — ступенчатая функция Хевисайда. Проще говоря: JR(i, j) = 1 тогда и только тогда, когда расстояние между x_i и x_j не превышает epsilonX, И расстояние между y_i и y_j не превышает epsilonY. Оба временных ряда должны иметь одинаковую длину, поскольку временные индексы i и j должны относиться к одним и тем же моментам времени в обеих системах.
Ключевое следствие состоит в том, что JRQA всегда строже, чем любой отдельный RQA. Коэффициент совместной рекуррентности всегда будет меньше или равен коэффициенту рекуррентности любого из временных рядов в отдельности, поскольку каждая точка совместной рекуррентности должна удовлетворять обоим условиям. Если X является высокорекуррентным, а Y — нет, то совместная матрица будет разреженной.
Каждому временному ряду требуется своё собственное значение эпсилон, чтобы анализ оставался независимым от масштаба. Если X торгуется около 1,08 с типичными движениями 0,0010, а Y — около 155 с типичными движениями 0,50, то использование одного и того же значения эпсилон для обоих рядов не имело бы смысла. Раздельные значения эпсилон позволяют настроить порог рекуррентности под собственный масштаб каждого временного ряда, после чего операция "И" определяет, когда оба ряда одновременно рекуррентны в своих соответствующих масштабах.
Матрица совместной рекуррентности
Чтобы построить матрицу совместной рекуррентности, необходимо выполнить вложение обоих временных рядов, вычислить попарные расстояния внутри каждого ряда, применить пороговую обработку к каждой матрице, а затем объединить их с помощью логического "И".
Шаг 1: выполнить вложение обоих временных рядов с одинаковыми параметрами
Пусть заданы временные ряды X и Y длиной L каждый. Мы выполняем вложение каждого из них с использованием одинаковой размерности вложения m и задержки τ. В результате из каждого временного ряда получается N = L – (m–1) * τ вложенных векторов. Параметры вложения должны быть идентичными, чтобы временные индексы совпадали: вектор i из X и вектор i из Y относятся к одному и тому же временному окну.
Шаг 2: Вычислить расстояния внутри каждого временного ряда
Для временного ряда X вычислите расстояние между каждой парой векторов X: dist_X(i, j) = ||x_i - x_j||. Для временного ряда Y вычислите dist_Y(i, j) = ||y_i - y_j||. Это те же расстояния, которые стандартный метод RQA вычислял бы для каждого временного ряда в отдельности.
Шаг 3: Применить к каждому набору расстояний собственное значение эпсилон
Примените epsilonX к расстояниям X, а epsilonY — к расстояниям Y. В результате получаются две двоичные матрицы саморекуррентности размером N×N: R_X(i, j) и R_Y(i, j).
Шаг 4: Выполнить логическое И для двух матриц
JR(i, j) = R_X(i, j) И R_Y(i, j)
Ячейка принимает значение 1 только в том случае, если в обеих базовых матрицах рекуррентности в этой позиции стоит 1. На практике библиотека не создает две отдельные булевы матрицы, чтобы затем выполнить для них логическое "И". Она вычисляет оба расстояния на лету и сохраняет только совместный результат, что позволяет экономить память и снижать нагрузку на кэш.

Рис. 3. Матрица совместной рекуррентности JR является результатом поэлементного логического "И" двух матриц саморекуррентности R_X и R_Y, что дает более разреженный результат
Получившаяся матрица является симметричной (JR(i, j) = JR(j, i)), поскольку обе исходные матрицы саморекуррентности являются симметричными. На главной диагонали находятся единицы, поскольку в обеих системах каждое состояние совпадает с самим собой. Эти свойства соответствуют стандартному RQA и отличают JRQA от CRQA, в котором ни одно из этих свойств не выполняется.
Метрики JRQA
Поскольку матрица совместной рекуррентности является квадратной и симметричной, как и стандартная матрица саморекуррентности, метод JRQA поддерживает полный набор из двенадцати метрик. В CRQA набор был ограничен десятью метриками, поскольку несимметричная, потенциально прямоугольная матрица перекрестной рекуррентности не поддерживает вычисления TREND и COMPLEXITY. В JRQA поддержка обеих метрик восстановлена.
| Метрика | Символ | Формула | Что измеряет |
|---|---|---|---|
| Коэффициент совместной рекуррентности | JRR | (точки совместной рекуррентности) / (N² - N) | Как часто обе системы одновременно возвращаются к своим прошлым состояниям. Строже, чем любой из индивидуальных коэффициентов рекуррентности (RR). |
| Совместный детерминизм | JDET | (точки на диагональных линиях длиной ≥ lmin) / (все точки совместной рекуррентности) | Доля точек совместной рекуррентности, образующих диагональные линии. Высокое значение JDET означает, что обе системы одновременно развиваются по параллельным траекториям. |
| Совместная ламинарность | JLAM | (точки на вертикальных линиях длиной >= vmin) / (все точки совместной рекуррентности) | Доля точек совместной рекуррентности, образующих вертикальные линии. Высокое значение JLAM означает, что обе системы одновременно находятся в состоянии захвата. |
| Совместное время захвата | JTT | Средняя длина вертикальных линий | Средняя продолжительность, в течение которой обе системы одновременно находятся в состоянии захвата. |
| Средняя длина совместной диагональной линии | JL | Средняя длина диагональных линий >= lmin | Средняя продолжительность синхронизированной детерминированной эволюции. |
| Максимальная длина совместной диагональной линии | JLmax | Самая длинная диагональная линия (за исключением главной диагонали) | Самый длинный участок, на котором обе системы одновременно развивались параллельно. |
| Максимальная длина вертикальной линии | JVmax | Самая длинная вертикальная линия | Наибольший период, в течение которого обе системы оставались в совместном состоянии захвата. |
| Совместная энтропия | JENTR | -sum(p(l) * ln(p(l))) | Сложность распределения диагональных линий совместной рекуррентности. Более высокая энтропия означает более разнообразную совместную детерминированную структуру. |
| Совместная дивергенция | JDIV | 1 / JLmax | Как быстро распадаются синхронизированные детерминированные сегменты. |
| Совместное отношение | JRATIO | JDET / JRR | Совместный детерминизм, нормированный по коэффициенту совместной рекуррентности. |
| Совместный тренд | JTREND | Коэффициент наклона регрессии плотности совместной рекуррентности по диагоналям | Нестационарность в совместной динамике. Положительное значение означает, что совместная рекуррентность возрастает для близких временных индексов. |
| Совместная сложность | JCOMPLEXITY | JRR * JDET | Комплексная метрика. Значение высокое, когда совместная динамика является одновременно высокорекуррентной и высокодетерминированной. |
Три наиболее полезные метрики для понимания совместной динамики — это JRR, JDET и JTREND.
JRR показывает, насколько выражена одновременная рекуррентность. Если JRR близок к минимальному значению индивидуального RR каждого временного ряда, то эти две системы в значительной степени независимы: их рекуррентные точки редко совпадают во времени. Если JRR близок к индивидуальным коэффициентам RR, системы тесно связаны: когда одна из них рекуррентна, другая почти всегда рекуррентна на тех же временных индексах.
JDET оценивает, образуют ли одновременные рекуррентные точки детерминированные паттерны. Высокий показатель JDET при низком показателе JRR означает, что те немногие точки совместной рекуррентности, которые имеются, имеют четко выраженную структуру. Эта закономерность обычно наблюдается, когда два инструмента в целом слабо связаны, но время от времени входят в совместный режим. Низкий показатель JDET в сочетании с умеренным показателем JRR означает частую, но неупорядоченную совместную рекуррентность, что указывает на совместное движение, обусловленное шумом, а не на детерминированную связь.
JTREND отслеживает изменения связи во времени. Положительное значение JTREND означает, что на недавних временных индексах наблюдается больше точек совместной рекуррентности, чем на более отдаленных, что свидетельствует о растущей синхронизации двух систем. Отрицательное значение JTREND означает ослабление связи. В торговле переход JTREND из отрицательной зоны в положительную может сигнализировать о том, что два инструмента входят в новый режим скоординированного поведения.
Эпсилон для совместной рекуррентности
В JRQA используются два значения эпсилон: по одному для каждого временного ряда. Такой подход является правильным в тех случаях, когда эти два временных ряда имеют разные масштабы, разную волатильность или разные единицы измерения. Порог "достаточно близко" для EURUSD не совпадает с порогом "достаточно близко" для USDJPY.
На практике существует три распространенные стратегии:
- Единое значение эпсилон после нормализации. Нормализуйте оба временных ряда к одному масштабу (z-оценка или логарифмические доходности с z-оценкой), а затем используйте для обоих одинаковое значение эпсилон. Именно так работает этот индикатор по умолчанию. После нормализации оба временных ряда имеют нулевое среднее и единичную дисперсию, поэтому единое значение эпсилон, равное 0,5, означает для обоих "в пределах половины стандартного отклонения". Метод SetEpsilon(double eps) фасада CJRQA в этом сценарии устанавливает оба порога в одно и то же значение.
- Раздельные значения эпсилон для исходных данных. При анализе исходных цен без нормализации задавайте каждое значение эпсилон с учетом масштаба соответствующего временного ряда. Для этого фасад предоставляет функцию SetEpsilon(double epsX, double epsY). Разумным начальным ориентиром является значение, равное 5 % от стандартного отклонения или диапазона каждого временного ряда.
- Значения эпсилон, откалиброванные по RR. Выполните стандартный RQA для каждого временного ряда по отдельности, используя бисекцию по целевому RR из CRQAEpsilon, чтобы найти значение эпсилон, при котором целевой RR для каждого ряда составляет 5 %. Затем передайте эти два значения эпсилон в вычисление JRQA. Это гарантирует, что каждый временной ряд вносит примерно равный вклад в плотность рекуррентности при совместном анализе.
В индикаторе используется первый подход: нормализация логарифмических доходностей с последующей z-оценкой и единым значением эпсилон. Это хорошо работает для разных валютных пар без необходимости ручной настройки. Для специализированного анализа, когда два временных ряда имеют очень разные масштабы или требуется точный контроль над плотностью рекуррентности каждого ряда, более подходящим является подход с раздельными значениями эпсилон.
Архитектура библиотеки
Расширение JRQA добавляет в библиотеку три новых заголовочных файла и класс-фасад. Они имеют ту же структурную схему, что и стандартные модули RQA и CRQA. Все модули JRQA подключаются автоматически при подключении файла RQA.mqh.

Рис. 4. Архитектура библиотеки JRQA: CJRQAMatrix лежит в основе и передает данные в CJRQAMetrics, который, в свою очередь, передает их в CJRQAWindow (с ускорением OpenCL) и фасад CJRQA на верхнем уровне
| Файл | Класс | Назначение |
|---|---|---|
| JRQAMatrix.mqh | CJRQAMatrix | Выполняет вложение двух временных рядов одинаковой длины, вычисляет матрицы расстояний каждого ряда с самим собой и строит матрицу совместной рекуррентности N×N через поэлементное логическое И двух пороговых матриц расстояний. |
| JRQAMetrics.mqh | CJRQAMetrics | Подсчитывает диагональные и вертикальные линии в матрице совместной рекуррентности. Вычисляет все двенадцать метрик JRQA и заполняет структуру SJRQAResult. |
| JRQAWindow.mqh | CJRQAWindow | Применяет JRQA в скользящем окне для двух параллельных временных рядов. Использует ускорение на GPU через OpenCL с пакетным запуском ядер, с автоматическим переходом к объединенному пути вычислений на CPU, если GPU недоступен. Содержит статические методы извлечения отдельных метрик. |
| RQA.mqh | CJRQA | Фасад высокого уровня. Настраивает и объединяет CJRQAMatrix и CJRQAMetrics в один вызов Compute(), который принимает два временных ряда одинаковой длины. |
Работу классов поддерживают две структуры:
- SJRQAResult содержит все двенадцать значений метрик, а также метод Reset(). Именно эту структуру заполняет CJRQAMetrics, и именно ее вы читаете после вычислений.
- SJRQAWindowResult связывает SJRQAResult с индексом бара, указывая, к какому окну относятся эти метрики.
Модули JRQA следуют соглашению об именовании, установленному в предыдущих статьях: префикс CJ + RQA для классов (CJRQAMatrix), префикс SJ + RQA для структур результатов (SJRQAResult). Фасадом является CJRQA. Все стандартные классы RQA и CRQA остаются без изменений и полностью обратно совместимыми. Единственная строка include остается прежней:
#include <RQA\RQA.mqh> Теперь подключение RQA.mqh дает доступ к классам стандартного RQA, перекрестного RQA и совместного RQA. Полная библиотека теперь содержит три полноценных пути анализа: RQA для саморекуррентности одного временного ряда, CCRQA для перекрестной рекуррентности двух временных рядов и CJRQA для совместной рекуррентности двух временных рядов.
CJRQAMatrix: построение матрицы совместной рекуррентности
CJRQAMatrix принимает два временных ряда одинаковой длины, выполняет вложение каждого из них в фазовое пространство, вычисляет расстояния каждого ряда с самим собой и формирует булеву матрицу совместной рекуррентности размером N×N с помощью операции AND над двумя результатами после пороговой обработки. Он хранит два вложенных массива отдельно и поддерживает независимые значения эпсилон для каждого временного ряда.
Структура класса
Класс хранит два вложенных массива (m_embX и m_embY), два значения эпсилон (m_epsilonX и m_epsilonY) и одну линеаризованную булеву матрицу m_R размером N×N. Как и стандартная матрица RQA, она использует построчную индексацию: элемент (i, j) соответствует индексу i * N + j.
//+------------------------------------------------------------------+ //| CJRQAMatrix — NxN joint recurrence matrix for two series | //| | //| JR(i,j) = 1 iff ||x_i - x_j|| <= epsX | //| AND ||y_i - y_j|| <= epsY | //+------------------------------------------------------------------+ class CJRQAMatrix { private: int m_N; // number of embedded vectors int m_embDim; int m_delay; double m_epsilonX; double m_epsilonY; ENUM_RQA_NORM m_norm; bool m_R[]; // flattened NxN joint boolean matrix double m_embX[]; // embedded X [N x embDim] double m_embY[]; // embedded Y [N x embDim] void Embed(const double &series[], int seriesLen, double &embedded[], int &numVec); double Distance(const double &emb[], int i, int j) const; public: bool Build(const double &seriesX[], const double &seriesY[], int seriesLen, double epsilonX, double epsilonY, int embDim = 1, int delay = 1, ENUM_RQA_NORM norm = RQA_NORM_EUCLIDEAN); bool Get(int i, int j) const; int Size() const { return m_N; } double EpsilonX() const { return m_epsilonX; } double EpsilonY() const { return m_epsilonY; } };
Сравните это с CCRQAMatrix из предыдущей статьи. В этом классе хранятся m_N и m_M (разные размеры для каждого временного ряда) и единое значение эпсилон. CJRQAMatrix хранит только m_N (оба временных ряда дают одинаковое количество вложенных векторов) и два раздельных значения эпсилон. Матрица всегда квадратная.
Вычисление расстояний
Метод Distance() принимает массив вложенных векторов в качестве параметра. В этом заключается ключевое структурное отличие как от CRQAMatrix (где индексация выполняется по единственному m_embedded), так и от CCRQAMatrix (где для одного операнда жестко задан m_embX, а для другого — m_embY). Здесь один и тот же метод вычисляет расстояния внутри любого из этих вложений, получая массив в качестве аргумента.
//+------------------------------------------------------------------+ //| Distance between embedded vectors i and j in a given embedding | //+------------------------------------------------------------------+ double CJRQAMatrix::Distance(const double &emb[], int i, int j) const { double dist = 0.0; for(int d = 0; d < m_embDim; d++) { double diff = emb[i * m_embDim + d] - emb[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; }
Такой параметризованный дизайн чище, чем использование отдельных методов DistanceX() и DistanceY(). Сама логика вычисления нормы идентична логике всех остальных методов расчета расстояний в библиотеке.
Построение матрицы
Метод Build() выполняет вложение обоих временных рядов, а затем за один проход заполняет булеву матрицу, проверяя оба условия расстояния непосредственно на месте.
//+------------------------------------------------------------------+ //| Build the NxN joint recurrence matrix | //| JR(i,j) = (||x_i-x_j|| <= epsX) AND (||y_i-y_j|| <= epsY) | //+------------------------------------------------------------------+ bool CJRQAMatrix::Build(const double &seriesX[], const double &seriesY[], int seriesLen, double epsilonX, double epsilonY, int embDim, int delay, ENUM_RQA_NORM norm) { if(seriesLen < 2 || epsilonX <= 0.0 || epsilonY <= 0.0 || embDim < 1 || delay < 1) { Print("JRQAMatrix::Build - invalid parameters"); return false; } m_epsilonX = epsilonX; m_epsilonY = epsilonY; m_embDim = embDim; m_delay = delay; m_norm = norm; int nX = 0, nY = 0; Embed(seriesX, seriesLen, m_embX, nX); Embed(seriesY, seriesLen, m_embY, nY); m_N = MathMin(nX, nY); if(m_N <= 0) { Print("JRQAMatrix::Build - series too short"); return false; } ArrayResize(m_R, m_N * m_N); for(int i = 0; i < m_N; i++) for(int j = 0; j < m_N; j++) m_R[i * m_N + j] = (Distance(m_embX, i, j) <= m_epsilonX) && (Distance(m_embY, i, j) <= m_epsilonY); return true; }
Ключевой является строка присваивания значения элементу матрицы. Каждая ячейка проверяет два условия расстояния и объединяет их логическим "И". Библиотека не создает две отдельные булевы матрицы с последующим их объединением. Она проверяет оба условия непосредственно на месте и сохраняет только итоговый совместный результат. Это позволяет сэкономить N² булевых значений во временном хранилище и избежать второго прохода по данным.
Проверка требует, чтобы оба значения эпсилон были положительными. Оба временных ряда используют один и тот же параметр seriesLen, что является ключевым ограничением JRQA: оба временных ряда должны иметь одинаковую длину. MathMin(nX, nY) служит мерой безопасности, но на практике значения nX и nY всегда будут равны, если seriesLen одинаков для обоих рядов.
CJRQAMetrics: количественная оценка совместной структуры
CJRQAMetrics принимает построенную CJRQAMatrix и извлекает из нее все двенадцать метрик. Эти алгоритмы идентичны алгоритмам в CRQAMetrics из первой статьи. Матрица квадратная и симметричная, поэтому к ней применим полный набор стандартных метрик RQA, включая TREND и COMPLEXITY, которые в CRQA невозможно было вычислить.
Структура SJRQAResult
//+------------------------------------------------------------------+ //| Struct — all JRQA results | //+------------------------------------------------------------------+ struct SJRQAResult { double JRR; double JDET; double JLAM; double JTT; double JL; double JLmax; double JVmax; double JENTR; double JDIV; double JRATIO; double JTREND; double JCOMPLEXITY; void Reset() { JRR=0; JDET=0; JLAM=0; JTT=0; JL=0; JLmax=0; JVmax=0; JENTR=0; JDIV=0; JRATIO=0; JTREND=0; JCOMPLEXITY=0; } };
Это повторяет структуру SRQAResult из первой статьи, но с префиксом "J" у каждого поля. Присутствуют все двенадцать метрик, включая JTREND и JCOMPLEXITY.
Подсчёт диагональных линий
Метод CountDiagonals() по структуре идентичен методу из CRQAMetrics. Он просматривает все 2*(N-1) диагонали (за исключением главной диагонали), отслеживает последовательности подряд идущих точек совместной рекуррентности и фиксирует длины линий в гистограмме.
//+------------------------------------------------------------------+ //| Count diagonal line lengths (excluding main diagonal) | //+------------------------------------------------------------------+ void CJRQAMetrics::CountDiagonals(const CJRQAMatrix &mat, int &lineLengths[]) const { int N = mat.Size(); ArrayResize(lineLengths, N + 1); ArrayInitialize(lineLengths, 0); for(int diag = -(N - 1); diag <= (N - 1); diag++) { if(diag == 0) continue; int len = 0; int iStart = MathMax(0, -diag); int iEnd = MathMin(N - 1, N - 1 - diag); for(int i = iStart; i <= iEnd; i++) { int j = i + diag; if(mat.Get(i, j)) len++; else { if(len >= m_minDiagLine && len < N) lineLengths[len]++; len = 0; } } if(len >= m_minDiagLine && len < N) lineLengths[len]++; } }
Ограничение len < N исключает любую линию, проходящую через всю ширину матрицы, что соответствует стандартному соглашению RQA. Диагональная линия длины l в матрице совместной рекуррентности означает, что обе системы одновременно развивались по параллельным траекториям в течение l последовательных временных шагов. Это более сильное утверждение, чем диагональная линия на любой из отдельных рекуррентных диаграмм.
Метод Compute()
Compute() проходит те же четыре этапа, что и CRQAMetrics::Compute(): (1) подсчёт точек совместной рекуррентности (JRR); (2) подсчёт диагональных линий (JDET, JL, JLmax, JENTR, JDIV); (3) подсчёт вертикальных линий (JLAM, JTT, JVmax); (4) вычисление агрегированных метрик (JRATIO, JCOMPLEXITY, JTREND). При подсчёте рекуррентных точек главная диагональ исключается (знаменатель равен N² – N), что соответствует стандартным соглашениям саморекуррентности.
CJRQAWindow: скользящий анализ совместной рекуррентности с ускорением на GPU
CJRQAWindow применяет JRQA в скользящем окне к двум параллельным временным рядам. Он использует ту же стратегию с приоритетом GPU и резервным выполнением на CPU, что и CCRQAWindow из предыдущей статьи, но ядро OpenCL отличается: вместо вычисления перекрёстных расстояний между X-векторами и Y-векторами оно вычисляет расстояния внутри каждого временного ряда и логически объединяет результаты операцией AND.
Ядро OpenCL
Ядро обрабатывает все окна за один пакетный запуск. Каждый рабочий элемент обрабатывает одну тройку (i, j, w), где i и j — индексы векторов в пределах окна, а w — индекс окна. Он вычисляет саморасстояние для X и саморасстояние для Y, а затем выдает 1 только в том случае, если оба расстояния находятся в пределах соответствующих порогов.
//+------------------------------------------------------------------+ //| OpenCL kernel — joint recurrence in a single pass | //| For each (i,j,w): compute distX and distY as self-distances, | //| output JR = (distX<=epsX) && (distY<=epsY) | //+------------------------------------------------------------------+ "__kernel void jrqa_recurrence( __global const float *seriesX, __global const float *seriesY, __global int *outR, const int N, const int embDim, const int tau, const int norm, const float epsilonX, const float epsilonY, const int step, const int baseWin) { int i = get_global_id(0); int j = get_global_id(1); int w = get_global_id(2); if(i >= N || j >= N) return; int winStart = (baseWin + w) * step; float distX = 0.0f, distY = 0.0f; // ... compute distX from seriesX, distY from seriesY ... float threshX = (norm==1) ? epsilonX*epsilonX : epsilonX; float threshY = (norm==1) ? epsilonY*epsilonY : epsilonY; outR[(w*N + i)*N + j] = ((distX <= threshX) && (distY <= threshY)) ? 1 : 0; }"
Ядро принимает оба значения эпсилон в качестве раздельных параметров. В случае евклидовой нормы (norm == 1) выполняется сравнение квадратов расстояний с квадратами пороговых значений, чтобы избежать вычисления квадратного корня на GPU. Буфер вывода хранит целые числа (0 или 1) для каждой ячейки каждого окна с индексацией вида outR[(w*N + i)*N + j].
Ключевое отличие от ядра CRQA заключается в том, что и distX, и distY являются саморасстояниями: seriesX[winStart+i] - seriesX[winStart+j] для distX и seriesY[winStart+i] - seriesY[winStart+j] для distY. Ядро CRQA вычисляло перекрестное расстояние seriesX[winStart+i] - seriesY[winStart+j]. Именно это изменение позволяет JRQA обнаруживать одновременную саморекуррентность, а не перекрестную рекуррентность.
Пакетная обработка на GPU
Метод RunGPU() использует ту же стратегию пакетной обработки, что и CCRQAWindow. Он вычисляет максимальный размер пакета на основе доступной памяти GPU (с ограничением буфера вывода 64 МБ), запускает пакеты окон, считывает обратно целочисленные результаты и передает их в функцию ScanMetrics(), которая вычисляет все двенадцать метрик JRQA на основе линейного массива целых чисел. Здесь без изменений повторно используется схема пакетного запуска.
Резервное выполнение на CPU: объединенные вычисления
Если OpenCL недоступен, функция ComputeFusedCPU() обрабатывает по одному окну за раз. Метод вычисляет оба саморасстояния непосредственно при сканировании диагональных и вертикальных линий, что позволяет избежать построения полной булевой матрицы. В типичном случае, когда embDim == 1 и используется евклидова норма, применяется быстрый путь выполнения со сравнением с квадратом эпсилон.
//+------------------------------------------------------------------+ //| CPU fallback — fused single-window joint recurrence compute | //+------------------------------------------------------------------+ void CJRQAWindow::ComputeFusedCPU( const double &sX[], int offX, const double &sY[], int offY, SJRQAResult &result) { result.Reset(); int N = m_windowSize - (m_embDim - 1) * m_delay; if(N <= 1) return; double epsSqX = m_epsilonX * m_epsilonX; double epsSqY = m_epsilonY * m_epsilonY; int diagHist[], vertHist[]; ArrayResize(diagHist, N + 1); ArrayResize(vertHist, N + 1); ArrayInitialize(diagHist, 0); ArrayInitialize(vertHist, 0); long recCount = 0; // Diagonal scan: fast path for embDim=1, Euclidean for(int k = -(N - 1); k <= (N - 1); k++) { if(k == 0) continue; 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 diffX = sX[offX + i] - sX[offX + i + k]; double diffY = sY[offY + i] - sY[offY + i + k]; if(diffX * diffX <= epsSqX && diffY * diffY <= epsSqY) { len++; recCount++; } else { if(len >= m_minDiagLine) diagHist[len]++; len = 0; } } if(len >= m_minDiagLine) diagHist[len]++; } // ... vertical scan and metric assembly follow ... }
В объединенном подходе саморасстояния для X и Y оцениваются непосредственно для каждой пары (i, j); при этом используются сравнения квадратов расстояний с квадратами порогов, чтобы избежать вызова MathSqrt(). При диагональном сканировании число рекуррентных точек накапливается одновременно с гистограммой линий. Вертикальное сканирование выполняется по той же схеме, но проходит вниз по столбцам. После завершения обоих сканирований метод вычисляет все двенадцать метрик на основе гистограмм, включая метрику JTREND, для которой требуется отдельный проход по верхним диагоналям.
Общая ветвь выполнения (embDim > 1 или неевклидовы нормы) использует полный цикл вычисления расстояний по всем измерениям вложения с проверками типа нормы во время выполнения. Это повторяет объединенный путь вычислений на CPU для CRQA из предыдущей статьи.
Статические экстракторы
Шесть статических экстракторов извлекают отдельные метрики из массива результатов: ExtractJRR, ExtractJDET, ExtractJLAM, ExtractJTT, ExtractJENTR и ExtractJLmax. Каждый из них следует одному и тому же шаблону однострочного кода, используемому во всей библиотеке.
//+------------------------------------------------------------------+ //| Extract single-metric arrays from windowed JRQA results | //+------------------------------------------------------------------+ void CJRQAWindow::ExtractJRR( const SJRQAWindowResult &r[], double &out[]) { int n = ArraySize(r); ArrayResize(out, n); for(int i = 0; i < n; i++) out[i] = r[i].metrics.JRR; }
CJRQA: Фасад
Фасад CJRQA оборачивает CJRQAMatrix и CJRQAMetrics в единый объект. В нём хранятся два значения эпсилон, параметры вложения и настройки нормы. Метод Compute() принимает два временных ряда одинаковой длины и возвращает все двенадцать метрик.
CJRQA jrqa; jrqa.SetEpsilon(0.5); jrqa.SetEmbedding(2, 1); if(jrqa.Compute(closeX, closeY, len)) jrqa.PrintSummary();
Предусмотрены две перегрузки метода SetEpsilon(). Версия с одним аргументом устанавливает одинаковое значение для обоих эпсилонов, что удобно, когда оба временных ряда приведены к одному и тому же масштабу. Версия с двумя аргументами настраивает их независимо друг от друга.
void SetEpsilon(double eps) { m_epsilonX = eps; m_epsilonY = eps; } void SetEpsilon(double epsX, double epsY) { m_epsilonX = epsX; m_epsilonY = epsY; }
Метод Compute() объединяет в одну цепочку построение матрицы и вычисление метрик, следуя той же схеме, что и все остальные фасады в библиотеке.
//+------------------------------------------------------------------+ //| Build joint recurrence matrix and compute all JRQA metrics | //+------------------------------------------------------------------+ bool CJRQA::Compute(const double &seriesX[], const double &seriesY[], int seriesLen) { m_computed = false; m_result.Reset(); if(seriesLen < 4) { Print("CJRQA::Compute - series too short"); return false; } if(!m_matrix.Build(seriesX, seriesY, seriesLen, m_epsilonX, m_epsilonY, 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, отдельные метрики становятся доступны через именованные методы доступа: JRR(), JDET(), JLAM(), JTT(), JENTR(), JDIV(), JLmax(), JVmax(), JRATIO(), JTREND(), JCOMPLEXITY(). Метод PrintSummary() выводит все двенадцать метрик в журнал "Эксперты", включая оба значения эпсилон и размеры матрицы.
Индикатор JRQA
Файл JRQA_Indicator.mq5 превращает библиотеку в инструмент совместного анализа в режиме реального времени. Он сравнивает символ текущего графика со вторым указанным пользователем символом (финансовым инструментом) и отображает пять метрик в отдельном окне: JRR, JDET, JLAM, JENTR и JTREND. Данный индикатор повторно использует тот же конвейер выравнивания временных меток и нормализации, что и индикатор CRQA, с добавлением буфера JTREND, который CRQA не поддерживал.
Настройка буферов и графиков
#property indicator_separate_window #property indicator_buffers 5 #property indicator_plots 5 #property indicator_label1 "JRR" #property indicator_type1 DRAW_LINE #property indicator_color1 clrDodgerBlue #property indicator_width1 2 #property indicator_label2 "JDET" #property indicator_type2 DRAW_LINE #property indicator_color2 clrLimeGreen #property indicator_width2 2 #property indicator_label3 "JLAM" #property indicator_type3 DRAW_LINE #property indicator_color3 clrOrange #property indicator_width3 2 #property indicator_label4 "JENTR" #property indicator_type4 DRAW_LINE #property indicator_color4 clrViolet #property indicator_width4 1 #property indicator_label5 "JTREND" #property indicator_type5 DRAW_LINE #property indicator_color5 clrRed #property indicator_width5 1
Пять буферов по сравнению с четырьмя в индикаторе CRQA. Дополнительным буфером является JTREND, который отображается красным цветом тонкой линией, чтобы отличать его от основных метрик. Цветовая схема соответствует соглашению, принятому в предыдущих статьях: синий цвет обозначает коэффициент рекуррентности, зеленый — детерминизм, оранжевый — ламинарность, фиолетовый — энтропию.
Входные параметры
#include <RQA\RQA.mqh> //--- Normalization options for cross-symbol JRQA enum ENUM_JRQA_NORMALIZE { JRQA_NORM_NONE = 0, // None (raw prices) JRQA_NORM_ZSCORE = 1, // Z-Score (recommended for cross-symbol) JRQA_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_JRQA_NORMALIZE InpNormalize = JRQA_NORM_RETURNS;
Входные параметры соответствуют индикатору CRQA. InpEpsilon — это единое значение эпсилон (используется для обоих временных рядов после нормализации). Перечисление нормализации определено локально и содержит те же три варианта: без нормализации, z-оценка и логарифмические доходности (с последующей z-оценкой). По умолчанию используются логарифмические доходности, которые хорошо работают для разных валютных пар.
Выравнивание временных меток и нормализация
В этом индикаторе используется тот же конвейер AlignPrices() с соединением слиянием и ApplyNormalization(), описанный в предыдущей статье. Оба временных ряда выравниваются по временным меткам, а затем нормализуются в соответствии с параметром InpNormalize. Режим логарифмических доходностей преобразует выровненные цены в логарифмические доходности, сдвигает карту баров на один шаг вперед и вычисляет z-оценки для обоих полученных временных рядов. Этот конвейер обеспечивает интерпретируемость значения эпсилон в единицах стандартного отклонения независимо от уровней цен инструментов.
Логика функции OnCalculate
Данный индикатор использует ту же стратегию инкрементального вычисления, что и индикатор CRQA. При полном пересчете он выравнивает всю историю данных, нормализует её, создает объект CJRQAWindow, запускает его и сопоставляет результаты с барами графика с помощью массива g_barMap. При инкрементальных обновлениях он заново выравнивает и нормализует всю историю, а затем пересчитывает только последние окна.
//+------------------------------------------------------------------+ //| Main calculation: align series, normalize, run JRQA windows | //+------------------------------------------------------------------+ if(fullRecalc) { if(!AlignPrices(time, close, rates_total)) return 0; ApplyNormalization(); CJRQAWindow win; SetupWindow(win); SJRQAWindowResult results[]; if(!win.Run(g_pricesX, g_validCount, g_pricesY, g_validCount, results)) return 0; for(int k = 0; k < ArraySize(results); k++) { int lastValid = results[k].barIndex + InpWindowSize - 1; if(lastValid < g_validCount) { int bar = g_barMap[lastValid]; BufferJRR[bar] = results[k].metrics.JRR; BufferJDET[bar] = results[k].metrics.JDET; BufferJLAM[bar] = results[k].metrics.JLAM; BufferJENTR[bar] = results[k].metrics.JENTR; BufferJTREND[bar] = results[k].metrics.JTREND; } } }
Результаты присваиваются последнему бару каждого окна (barIndex + InpWindowSize - 1), а массив g_barMap преобразует выровненный индекс обратно в фактическую позицию бара на графике. То же соглашение используется в индикаторах RQA и CRQA.

Рис. 5. Диалоговое окно настроек индикатора JRQA с указанием второго символа, эпсилона, размера окна, режима нормализации и других входных параметров

Рис. 6. Индикатор JRQA, сравнивающий GBPUSD с EURUSD с использованием нормализации по логарифмическим доходностям; JRR, JDET, JLAM, JENTR и JTREND построены в отдельном окне индикатора
Что дальше?
Благодаря внедрению стандартного RQA, перекрестного RQA и совместного RQA библиотека теперь охватывает три основных аспекта анализа рекуррентности: саморекуррентность внутри одного временного ряда, перекрестную рекуррентность между двумя временными рядами и одновременную саморекуррентность в двух временных рядах. Набор аналитических инструментов готов. Логичным следующим шагом будет применить это на практике.
- Использование скользящего JRQA для коррелированных пар с целью выявления в реальном времени сопряженности режимов и их рассогласования, где JTREND выступает в качестве опережающего сигнала.
- Объединение метрик JRR/JDET из JRQA с метриками CRR/CDET из CRQA для различения перекрытия в пространстве состояний и синхронизированного поведения режимов.
- Построение композитного индикатора режима, объединяющего стандартный количественный анализ рекуррентности (RQA) (сложность одного временного ряда), количественный анализ перекрестной рекуррентности (CRQA) (связь между временными рядами) и JRQA (синхронизированная динамика) в многомерное состояние режима.
- Использование полного набора метрик RQA, CRQA и JRQA в качестве признаков в модели классификации для обнаружения режимов или генерации сигналов.
- Применение JRQA между ценой и объемом для выявления периодов, когда оба ряда одновременно находятся в структурированных, детерминированных режимах.
Библиотека была разработана именно для такого многоуровневого анализа. Все три направления анализа используют одну и ту же инфраструктуру вложения, нормы и конфигурации. Следующая статья из этого цикла будет посвящена применению этих инструментов в реальной торговой логике.
Заключение
Количественный анализ совместной рекуррентности (JRQA) добавляет третье аналитическое измерение в библиотеку RQA. В то время как стандартный RQA измеряет внутреннюю рекуррентность в пределах одного временного ряда, а CRQA — общую динамику между двумя временными рядами, JRQA выявляет случаи, когда две системы одновременно возвращаются к своему собственному прошлому. Матрица совместной рекуррентности представляет собой поэлементное логическое И двух матриц саморекуррентности и всегда строже, чем любая из них по отдельности. В данной статье представлена полная реализация на языке MQL5, состоящая из трёх новых модулей и обновлённого фасада:
- CJRQAMatrix выполняет вложение двух временных рядов одинаковой длины, вычисляет саморасстояния для каждого из них с использованием трёх норм расстояния и формирует матрицу совместной рекуррентности размером N×N посредством встроенной операции AND над двумя результатами после пороговой обработки.
- CJRQAMetrics извлекает двенадцать метрик из матрицы совместной рекуррентности, охватывающих диагональную структуру (JDET, JL, JLmax, JENTR, JDIV), вертикальную структуру (JLAM, JTT, JVmax), агрегированные метрики (JRR, JRATIO), а также две метрики, которые CRQA не мог поддерживать: JTREND и JCOMPLEXITY.
- CJRQAWindow применяет JRQA в скользящем окне с использованием вычислений, ускоренных на GPU, посредством пользовательского ядра OpenCL, которое за один проход оценивает оба условия саморекуррентности. В системах без поддержки GPU происходит автоматический переход на объединённый путь вычислений на CPU.
- CJRQA предоставляет высокоуровневый фасад с поддержкой раздельных значений эпсилон для каждого временного ряда, что упрощает независимый от масштаба совместный анализ.
Прилагаемый индикатор в режиме реального времени строит графики метрик JRR, JDET, JLAM, JENTR и JTREND между символом графика и любым вторым символом, с корректным выравниванием временных меток, встроенной нормализацией (логарифмические доходности с z-оценкой) и инкрементным вычислением для торговли в реальном времени.
Полный исходный код к этой статье также доступен на Algo Forge.
Все модули обратно совместимы с библиотеками RQA и CRQA, описанными в первой и второй статьях. Достаточно подключить один include-файл RQA.mqh, чтобы получить доступ ко всему.
| # | Имя файла | Тип | Описание |
|---|---|---|---|
| 1 | RQA.mqh | Подключаемый файл | Обновлённый основной include-файл с фасадами CRQA, CCRQA и CJRQA |
| 2 | JRQAMatrix.mqh | Подключаемый файл | Класс CJRQAMatrix: матрица совместной рекуррентности размером N×N, построенная на основе двух временных рядов одинаковой длины |
| 3 | JRQAMetrics.mqh | Подключаемый файл | Класс CJRQAMetrics и структура SJRQAResult: двенадцать метрик JRQA |
| 4 | JRQAWindow.mqh | Подключаемый файл | Класс CJRQAWindow: JRQA в скользящем окне с ускорением на GPU с помощью OpenCL и резервным выполнением на CPU |
| 5 | JRQA_Indicator.mq5 | Индикатор | Построение графиков JRR, JDET, JLAM, JENTR и JTREND между двумя символами (финансовыми инструментами) с выравниванием по временным меткам и нормализацией |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22610
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Нейросети в трейдинге: Диффузионная генерация торговых планов (Окончание)
Неопределённость как модель (Часть 9): Процессы авторегрессии
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования