preview
Joint Recurrence Quantification Analysis (JRQA) in MQL5: Detecting Simultaneous Recurrence in Two Series

Joint Recurrence Quantification Analysis (JRQA) in MQL5: Detecting Simultaneous Recurrence in Two Series

MetaTrader 5Indicators |
546 0
Hammad Dilber
Hammad Dilber

Contents

  1. Introduction
  2. JRQA on a Chart
  3. From Cross-Recurrence to Joint Recurrence
  4. The Joint Recurrence Matrix
  5. JRQA Metrics
  6. Epsilon for Joint Recurrence
  7. Library Architecture
  8. CJRQAMatrix: Building the Joint Recurrence Matrix
  9. CJRQAMetrics: Quantifying Joint Structure
  10. CJRQAWindow: Rolling Joint Analysis with GPU Acceleration
  11. CJRQA: The Facade
  12. The JRQA Indicator
  13. What's Next
  14. Conclusion


Introduction

The first article in this series built an RQA library that analyzes a single time series against its own past. The second article extended it with Cross-Recurrence Quantification Analysis, which compares the phase-space trajectories of two different series. CRQA answers the question: when series X is in state x_i, is series Y ever in a similar state y_j? That is a useful question, but it is not the only question you can ask about two systems.

This article adds a third perspective: Joint Recurrence Quantification Analysis (JRQA). Instead of asking whether X and Y visit similar states, JRQA asks whether X and Y simultaneously revisit their own respective pasts at the same time indices. A joint recurrence at (i, j) means that X at time i is close to X at time j, AND Y at time i is close to Y at time j. Both systems must recur at the same pair of time points. Neither one alone is sufficient.

This distinction matters for trading. CRQA detects shared state-space territory. JRQA detects synchronized regime behavior. Two instruments can visit similar price levels (high CRQA) without being in the same dynamical regime at the same time. Conversely, two instruments can be locked in synchronized regimes (high JRQA) while trading at completely different scales. JRQA captures the latter pattern.

This article adds three modules (CJRQAMatrix, CJRQAMetrics, CJRQAWindow) and updates the CJRQA facade in RQA.mqh. It also includes an indicator that plots joint-recurrence metrics for two symbols in real time. The rolling-window module includes OpenCL GPU acceleration with automatic CPU-fallback. As with the previous articles, no trading strategy is built here. The goal is a tested analytical layer that you can incorporate into experiments.


JRQA on a Chart

The image below shows the JRQA indicator comparing two symbols on a live chart. Five metrics are plotted in a separate window: Joint Recurrence Rate (JRR), Joint Determinism (JDET), Joint Laminarity (JLAM), Joint Entropy (JENTR), and Joint Trend (JTREND). Each one is computed over a rolling window of the two aligned and normalized series.

Fig. 1. The JRQA indicator in a separate window, plotting JRR, JDET, JLAM, JENTR, and JTREND between two symbols in real time

A high JRR means both systems revisit their own past states at the same indices. A high JDET means these joint recurrences form diagonal lines, indicating parallel evolution. When JLAM spikes, both systems are jointly trapped in their respective states. A rising JTREND means joint recurrence is becoming more frequent for nearby time indices compared to distant ones, signaling a regime shift in the coupling between the two systems.

That is what the library produces. The rest of this article explains how.


From Cross-Recurrence to Joint Recurrence

CRQA and JRQA both analyze two time series, but they ask fundamentally different questions and produce structurally different matrices.

In CRQA, we embed X and Y into the same phase space and compare X-vectors against Y-vectors. The cross-recurrence matrix CR(i, j) asks: is the state of X at time i close to the state of Y at time j? The matrix is N×M (potentially rectangular), not symmetric, and has no guaranteed main diagonal. It measures shared state-space territory.

In JRQA, we build two separate self-recurrence matrices and take their element-wise AND. The joint recurrence matrix JR(i, j) asks: is X at time i close to X at time j, AND is Y at time i close to Y at time j? Both conditions must hold simultaneously. The matrix is N×N (always square), symmetric, and has a main diagonal of ones (every state is identical to itself in both systems). It measures synchronized dynamical behavior.

Fig. 2. CRQA compares X against Y in one matrix (left); JRQA computes two self-recurrence matrices and ANDs them (right)

The formal definition is:

JR(i, j) = Theta(epsilonX - ||x_i - x_j||) * Theta(epsilonY - ||y_i - y_j||)

where Theta is the Heaviside step function. In plain terms: JR(i, j) = 1 if and only if the distance between x_i and x_j is within epsilonX, AND the distance between y_i and y_j is within epsilonY. Both series must be of equal length, because the time indices i and j must refer to the same moments for both systems.

A key consequence is that JRQA is always stricter than either individual RQA. The joint recurrence rate will always be less than or equal to the recurrence rate of either series alone, because every joint recurrence point must satisfy both conditions. If X is highly recurrent but Y is not, the joint matrix will be sparse.

Each series needs its own epsilon to keep the analysis scale-independent. If X trades around 1.08 with typical moves of 0.0010 and Y trades around 155 with typical moves of 0.50, using the same epsilon for both would be meaningless. Separate epsilons let you calibrate the recurrence threshold to each series' own scale, and then the AND operation detects when both are simultaneously recurrent at their respective scales.


The Joint Recurrence Matrix

To build the joint recurrence matrix: embed both series, compute self-distances, threshold each matrix, then combine them with a logical AND.

Step 1: Embed both series with the same parameters

Given series X and Y, both of length L, we embed each one using the same embedding dimension m and delay tau. This produces N = L - (m-1)*tau embedded vectors from each series. The embedding parameters must be identical so that the time indices align: vector i from X and vector i from Y refer to the same time window.

Step 2: Compute self-distances for each series

For series X, compute the distance between every pair of X-vectors: dist_X(i, j) = ||x_i - x_j||. For series Y, compute dist_Y(i, j) = ||y_i - y_j||. These are the same distances that standard RQA would compute for each series individually.

Step 3: Threshold each with its own epsilon

Apply epsilonX to the X-distances and epsilonY to the Y-distances. This produces two N×N binary self-recurrence matrices: R_X(i, j) and R_Y(i, j).

Step 4: AND the two matrices

JR(i, j) = R_X(i, j) AND R_Y(i, j)

A cell is 1 only if both underlying recurrence matrices have a 1 at that position. In practice, the library does not build two separate boolean matrices and then AND them. It computes both distances inline and stores only the joint result, saving memory and cache pressure.

Fig. 3. The joint recurrence matrix JR is the element-wise AND of two self-recurrence matrices R_X and R_Y, producing a sparser result

The resulting matrix is symmetric (JR(i, j) = JR(j, i)) because both underlying self-recurrence matrices are symmetric. It has ones on the main diagonal because every state is identical to itself in both systems. These properties mirror standard RQA and distinguish JRQA from CRQA, where neither property holds.


JRQA Metrics

Because the joint recurrence matrix is square and symmetric, like the standard self-recurrence matrix, JRQA supports the full set of twelve metrics. CRQA was limited to ten because the non-symmetric, potentially rectangular cross-recurrence matrix does not support the TREND and COMPLEXITY computations. JRQA restores both.

Metric
Symbol
Formula
What It Measures
Joint Recurrence Rate
JRR
(joint recurrent points) / (N^2 - N)
How often both systems simultaneously revisit their own past states. Stricter than either individual RR.
Joint Determinism
JDET
(points on diagonal lines >= lmin) / (all joint recurrent points)
Fraction of joint recurrences forming diagonal lines. High JDET means both systems are simultaneously evolving through parallel trajectories.
Joint Laminarity
JLAM
(points on vertical lines >= vmin) / (all joint recurrent points)
Fraction of joint recurrences forming vertical lines. High JLAM means both systems are simultaneously trapped.
Joint Trapping Time
JTT
Average length of vertical lines
Average duration that both systems remain simultaneously trapped.
Avg Joint Diagonal
JL
Average length of diagonal lines >= lmin
Average duration of synchronized deterministic evolution.
Max Joint Diagonal
JLmax
Longest diagonal line (excl. main diagonal)
Longest stretch where both systems evolved in parallel simultaneously.
Max Vertical Line
JVmax
Longest vertical line
Longest period both systems remained jointly trapped.
Joint Entropy
JENTR
-sum(p(l) * ln(p(l)))
Complexity of the joint diagonal line distribution. Higher entropy means more varied joint deterministic structure.
Joint Divergence
JDIV
1 / JLmax
How quickly synchronized deterministic segments break down.
Joint Ratio
JRATIO
JDET / JRR
Joint determinism normalized by joint recurrence rate.
Joint Trend
JTREND
Regression slope of diagonal-wise joint recurrence density
Non-stationarity in the joint dynamics. Positive means joint recurrence is increasing for nearby time indices.
Joint Complexity
JCOMPLEXITY
JRR * JDET
Composite measure. High when joint dynamics are both highly recurrent and highly deterministic.

The three most useful metrics for understanding joint dynamics are JRR, JDET, and JTREND.

JRR tells you how much simultaneous recurrence exists. If JRR is close to the minimum of each series' individual RR, the two systems are largely independent: their recurrences rarely overlap in time. If JRR is close to the individual RRs, the systems are tightly coupled: when one recurs, the other almost always recurs at the same time indices.

JDET measures whether the simultaneous recurrences form deterministic patterns. High JDET with low JRR means that the few joint recurrences that exist are highly structured. This pattern typically appears when two instruments are loosely coupled overall but occasionally lock into a shared regime. Low JDET with moderate JRR means frequent but disorganized joint recurrence, suggesting noise-driven co-movement rather than deterministic coupling.

JTREND detects changes in the coupling over time. A positive JTREND means that recent time indices show more joint recurrence than distant ones, signaling that the two systems are becoming more synchronized. A negative JTREND means the coupling is weakening. In trading, a shift from negative to positive JTREND could signal that two instruments are entering a new regime of coordinated behavior.


Epsilon for Joint Recurrence

JRQA takes two epsilon values, one for each series. This is the correct approach when the two series have different scales, different volatilities, or different units. The threshold for "close enough" in EURUSD is not the same as the threshold for "close enough" in USDJPY.

In practice, there are three common strategies:

  • Shared epsilon after normalization. Normalize both series to the same scale (z-score or log returns with z-score), then use the same epsilon for both. This is what the indicator does by default. After normalization, both series have zero mean and unit variance, so a single epsilon of 0.5 means "within half a standard deviation" for both. The CJRQA facade's SetEpsilon(double eps) method sets both thresholds to the same value for this use case.
  • Separate epsilons for raw data. When analyzing raw prices without normalization, set each epsilon to a value appropriate for that series' scale. The facade provides SetEpsilon(double epsX, double epsY) for this. A reasonable starting point is 5% of each series' standard deviation or range.
  • RR-calibrated epsilons. Run standard RQA on each series individually, using the RR-target bisection from CRQAEpsilon to find the epsilon that gives a target RR of 5% for each. Then pass those two epsilons to the JRQA computation. This ensures that each series contributes roughly equal recurrence density to the joint analysis.

The indicator uses the first approach: log-returns normalization followed by z-score, with a shared epsilon. This works well across different currency pairs without manual tuning. For specialized analyses where the two series are at very different scales or where you want fine control over each series' recurrence density, the separate-epsilon approach is more appropriate.


Library Architecture

The JRQA extension adds three new header files and a facade class to the library. They follow the same structural pattern as the standard RQA and CRQA modules. All JRQA modules are included automatically when you include RQA.mqh.

Fig. 4. JRQA library architecture: CJRQAMatrix at the base feeds CJRQAMetrics, which feeds CJRQAWindow (with OpenCL acceleration) and the CJRQA facade at the top

File
Class
Responsibility
JRQAMatrix.mqh
CJRQAMatrix
Embeds two equal-length series, computes self-distances for each, and builds the N×N joint recurrence matrix via element-wise AND of the two thresholded distance matrices.
JRQAMetrics.mqh
CJRQAMetrics
Counts diagonal and vertical lines in the joint recurrence matrix. Computes all twelve JRQA metrics and fills the SJRQAResult struct.
JRQAWindow.mqh
CJRQAWindow
Applies JRQA over a rolling window on two parallel series. Uses OpenCL GPU acceleration with batched kernel execution, falling back to a fused CPU path when no GPU is available. Includes static extractors for individual metrics.
RQA.mqh
CJRQA
High-level facade. Configures and chains CJRQAMatrix and CJRQAMetrics into a single Compute() call that takes two equal-length series.

Two structs support the classes:

  • SJRQAResult holds all twelve metric values plus a Reset() method. This is what CJRQAMetrics fills and what you read after computation.
  • SJRQAWindowResult pairs an SJRQAResult with a bar index, identifying which window the metrics belong to.

The JRQA modules follow the naming convention established in the previous articles: CJ + RQA prefix for classes (CJRQAMatrix), SJ + RQA prefix for result structs (SJRQAResult). The facade is CJRQA. All standard RQA and CRQA classes remain unchanged and fully backward-compatible. The single include line remains:

#include <RQA\RQA.mqh>

Including RQA.mqh now gives you access to standard RQA, cross-RQA, and joint-RQA classes. The full library now contains three complete analysis paths: RQA for single-series self-recurrence, CCRQA for two-series cross-recurrence, and CJRQA for two-series joint recurrence.


CJRQAMatrix: Building the Joint Recurrence Matrix

CJRQAMatrix takes two equal-length series, embeds each one into phase space, computes self-distances for each, and produces the N×N joint boolean matrix by ANDing the two thresholded results. It stores the two embedded arrays separately and supports independent epsilon values for each series.

Class Structure

The class stores two embedded arrays (m_embX and m_embY), two epsilon values (m_epsilonX and m_epsilonY), and one flattened boolean matrix m_R of size N×N. Like the standard RQA matrix, it uses row-major indexing: element (i, j) maps to index 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; }
  };

Compare this to CCRQAMatrix from the previous article. That class stores m_N and m_M (different sizes for each series) and one shared epsilon. CJRQAMatrix stores only m_N (both series produce the same number of embedded vectors) and two separate epsilons. The matrix is always square.

Distance Computation

The Distance() method takes the embedded array as a parameter. This is the key structural difference from both CRQAMatrix (which indexes into a single m_embedded) and CCRQAMatrix (which hardcodes m_embX for one operand and m_embY for the other). Here, the same method computes distances within either embedding by receiving the array as an argument.

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

This parameterized design is cleaner than having separate DistanceX() and DistanceY() methods. The norm logic itself is identical to every other distance method in the library.

Building the Matrix

The Build() method embeds both series, then fills the boolean matrix in a single pass by evaluating both distance conditions inline.

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

The critical line is the matrix assignment. Each cell evaluates two distance conditions and ANDs them. The library does not build two separate boolean matrices and then combine them. It evaluates both conditions inline and stores only the final joint result. This saves N^2 booleans of temporary storage and avoids a second pass over the data.

The validation requires both epsilons to be positive. Both series use the same seriesLen parameter, which is the key constraint of JRQA: both series must have the same length. The MathMin(nX, nY) is a safety measure, but in practice nX and nY will always be equal when seriesLen is the same for both.


CJRQAMetrics: Quantifying Joint Structure

CJRQAMetrics takes a built CJRQAMatrix and extracts all twelve metrics. The algorithms are identical to those in CRQAMetrics from the first article. The matrix is square and symmetric, so the full set of standard RQA metrics applies, including TREND and COMPLEXITY which CRQA could not compute.

The SJRQAResult Struct

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

This mirrors SRQAResult from the first article with the J prefix on every field. All twelve metrics are present, including JTREND and JCOMPLEXITY.

Diagonal Line Counting

The CountDiagonals() method is structurally identical to the one in CRQAMetrics. It scans all 2*(N-1) diagonals (excluding the main diagonal), tracks runs of consecutive joint recurrence points, and records line lengths in a histogram.

//+------------------------------------------------------------------+
//| 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]++;
     }
  }

The len < N cap excludes any line spanning the full matrix width, consistent with the standard RQA convention. A diagonal line of length l in the joint recurrence matrix means that both systems simultaneously evolved through parallel trajectories for l consecutive time steps. This is a stronger statement than a diagonal line in either individual recurrence plot.

The Compute Method

Compute() follows the same four stages as CRQAMetrics::Compute(): (1) count recurrent points (JRR); (2) count diagonals (JDET, JL, JLmax, JENTR, JDIV); (3) count verticals (JLAM, JTT, JVmax); (4) compute aggregates (JRATIO, JCOMPLEXITY, JTREND). The recurrence count excludes the main diagonal (denominator is N^2 - N), consistent with standard self-recurrence conventions.


CJRQAWindow: Rolling Joint Analysis with GPU Acceleration

CJRQAWindow applies JRQA over a rolling window on two parallel series. It follows the same GPU-first, CPU-fallback strategy as CCRQAWindow from the previous article, but the OpenCL kernel is different: instead of computing cross-distances between X-vectors and Y-vectors, it computes self-distances within each series and ANDs the results.

The OpenCL Kernel

The kernel processes all windows in a single batched dispatch. Each work item handles one (i, j, w) triple, where i and j are vector indices within the window and w is the window index. It computes the self-distance for X and the self-distance for Y, then outputs 1 only if both are within their respective thresholds.

//+------------------------------------------------------------------+
//| 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;
}"

The kernel takes both epsilons as separate parameters. For the Euclidean norm (norm == 1), it compares squared distances against squared thresholds to avoid the GPU sqrt. The output buffer stores integers (0 or 1) for each cell of each window, indexed as outR[(w*N + i)*N + j].

The key difference from the CRQA kernel is that both distX and distY are self-distances: seriesX[winStart+i] - seriesX[winStart+j] for distX, and seriesY[winStart+i] - seriesY[winStart+j] for distY. The CRQA kernel computed the cross-distance seriesX[winStart+i] - seriesY[winStart+j]. This change is what makes JRQA detect simultaneous self-recurrence rather than cross-recurrence.

GPU Batching

The RunGPU() method uses the same batching strategy as CCRQAWindow. It computes the maximum batch size based on available GPU memory (capped at 64 MB of output buffer), dispatches batches of windows, reads back the integer results, and feeds them to ScanMetrics() which derives all twelve JRQA metrics from the flat integer array. This reuses the batched dispatch pattern without change.

CPU Fallback: Fused Computation

When OpenCL is unavailable, ComputeFusedCPU() processes one window at a time. It computes both self-distances inline during the diagonal and vertical line scans, avoiding the need to build a full boolean matrix. For the common case of embDim == 1 with Euclidean norm, it uses a fast path with squared-epsilon comparison.

//+------------------------------------------------------------------+
//| 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 ...
  }

The fused approach evaluates both the X self-distance and the Y self-distance inline at each (i, j) pair, using squared comparisons to avoid MathSqrt(). For the diagonal scan, the recurrence count is accumulated alongside the line histogram. The vertical scan follows the same pattern but iterates down columns. After both scans, the method derives all twelve metrics from the histograms, including JTREND which requires a separate pass over the upper diagonals.

The general path (embDim > 1 or non-Euclidean norms) uses the full distance loop over embedding dimensions, with runtime checks for the norm type. This mirrors the CRQA fused CPU path from the previous article.

Static Extractors

Six static extractors pull individual metrics from the results array: ExtractJRR, ExtractJDET, ExtractJLAM, ExtractJTT, ExtractJENTR, and ExtractJLmax. Each follows the same one-liner pattern used throughout the library.

//+------------------------------------------------------------------+
//| 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: The Facade

The CJRQA facade wraps CJRQAMatrix and CJRQAMetrics into a single object. It stores the two epsilon values, embedding parameters, and norm configuration. The Compute() method takes two equal-length series and returns all twelve metrics.

CJRQA jrqa;
jrqa.SetEpsilon(0.5);
jrqa.SetEmbedding(2, 1);
if(jrqa.Compute(closeX, closeY, len))
   jrqa.PrintSummary();

Two SetEpsilon() overloads are provided. The single-argument version sets both epsilons to the same value, which is convenient when both series are normalized to the same scale. The two-argument version sets them independently.

void SetEpsilon(double eps)
  { m_epsilonX = eps; m_epsilonY = eps; }
void SetEpsilon(double epsX, double epsY)
  { m_epsilonX = epsX; m_epsilonY = epsY; }

The Compute() method chains the matrix build and metrics computation, following the same pattern as every other facade in the library.

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

After Compute() returns true, individual metrics are available through named accessors: JRR(), JDET(), JLAM(), JTT(), JENTR(), JDIV(), JLmax(), JVmax(), JRATIO(), JTREND(), JCOMPLEXITY(). The PrintSummary() method outputs all twelve metrics to the Experts log, including both epsilon values and the matrix dimensions.


The JRQA Indicator

The JRQA_Indicator.mq5 file turns the library into a live joint analysis tool. It compares the current chart symbol against a user-specified second symbol and plots five metrics in a separate window: JRR, JDET, JLAM, JENTR, and JTREND. The indicator reuses the same timestamp alignment and normalization pipeline from the CRQA indicator, with the addition of the JTREND buffer that CRQA did not support.

Buffer and Plot Setup

#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

Five buffers compared to the CRQA indicator's four. The extra buffer is JTREND, plotted in red with thin width to distinguish it from the core metrics. The color scheme follows the convention established in the previous articles: blue for recurrence rate, green for determinism, orange for laminarity, violet for entropy.

Input Parameters

#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;

The input parameters mirror the CRQA indicator. InpEpsilon is a shared epsilon (used for both series after normalization). The normalization enum is defined locally with the same three options: no normalization, z-score, and log returns (with z-score on top). The default is log returns, which works well across different currency pairs.

Timestamp Alignment and Normalization

The indicator uses the same AlignPrices() merge-join and ApplyNormalization() pipeline described in the previous article. Both series are aligned by timestamp, then normalized according to InpNormalize. The log-returns mode converts aligned prices to log returns, shifts the bar map forward by one, and z-scores both resulting series. This pipeline ensures that epsilon is interpretable in standard-deviation units regardless of the instruments' price levels.

The OnCalculate Logic

The indicator uses the same incremental computation strategy as the CRQA indicator. On full recalculation, it aligns the entire history, normalizes, creates a CJRQAWindow, runs it, and maps the results back to chart bars via the g_barMap array. On incremental updates, it realigns and renormalizes the full history, then recomputes only the trailing windows.

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

Results are assigned to the last bar of each window (barIndex + InpWindowSize - 1), and the g_barMap translates the aligned index back to the actual chart bar position. This is the same convention used in the RQA and CRQA indicators.

Fig. 5. The JRQA indicator settings dialog showing the second symbol, epsilon, window size, normalization mode, and other inputs

Fig. 6. The JRQA indicator comparing GBPUSD with EURUSD using log-returns normalization, with JRR, JDET, JLAM, JENTR, and JTREND plotted in the separate indicator window


What's Next

With standard RQA, cross-RQA, and joint-RQA in place, the library now covers the three fundamental recurrence analysis perspectives: self-recurrence within one series, cross-recurrence between two series, and simultaneous self-recurrence in two series. The analytical toolkit is complete. The natural next step is to put it to work.

  • Using rolling JRQA between correlated pairs to detect regime coupling and decoupling in real time, with JTREND as the leading signal.
  • Combining JRR/JDET from JRQA with CRR/CDET from CRQA to distinguish between shared state-space overlap and synchronized regime behavior.
  • Building a composite regime indicator that fuses standard RQA (single-series complexity), CRQA (cross-series coupling), and JRQA (synchronized dynamics) into a multi-dimensional regime state.
  • Feeding the full set of RQA, CRQA, and JRQA metrics as features into a classification model for regime detection or signal generation.
  • Applying JRQA between price and volume to detect periods where both are simultaneously in structured, deterministic regimes.

The library was designed for this kind of layered analysis. All three analysis paths share the same embedding, norm, and configuration infrastructure. The next article in this series will focus on applying these tools to actual trading logic.


Conclusion

Joint Recurrence Quantification Analysis adds a third analytical dimension to the RQA library. While standard RQA measures internal recurrence within one series and CRQA measures shared dynamics between two series, JRQA detects when two systems simultaneously revisit their own respective pasts. The joint recurrence matrix is the element-wise AND of two self-recurrence matrices, and it is always stricter than either one alone. This article presented a complete MQL5 implementation consisting of three new modules and an updated facade:

  • CJRQAMatrix embeds two equal-length series, computes self-distances for each using three distance norms, and produces the N×N joint recurrence matrix via inline AND of the two thresholded results.
  • CJRQAMetrics extracts twelve metrics from the joint recurrence matrix, covering diagonal structure (JDET, JL, JLmax, JENTR, JDIV), vertical structure (JLAM, JTT, JVmax), aggregates (JRR, JRATIO), and the two metrics that CRQA could not support: JTREND and JCOMPLEXITY.
  • CJRQAWindow applies JRQA over a rolling window using GPU-accelerated computation via a custom OpenCL kernel that evaluates both self-recurrence conditions in a single pass. Falls back automatically to a fused CPU path for systems without GPU support.
  • CJRQA provides a high-level facade with support for independent epsilons per series, making scale-independent joint analysis straightforward.

The accompanying indicator plots JRR, JDET, JLAM, JENTR, and JTREND between the chart symbol and any second symbol in real time, with proper timestamp alignment, built-in normalization (log returns with z-score), and incremental computation for live trading.

All modules are backward-compatible with the RQA and CRQA libraries from the first and second articles. A single include of RQA.mqh gives access to everything.

 # File name
 Type Description
RQA.mqh
 Include Updated main include file with CRQA, CCRQA, and CJRQA facades
JRQAMatrix.mqh
 Include CJRQAMatrix class: N×N joint recurrence matrix from two equal-length series
JRQAMetrics.mqh
 Include CJRQAMetrics class and SJRQAResult struct: twelve JRQA metrics
JRQAWindow.mqh
 Include CJRQAWindow class: rolling JRQA with OpenCL GPU acceleration and CPU-fallback
5 JRQA_Indicator.mq5
 Indicator Plots JRR, JDET, JLAM, JENTR, JTREND between two symbols with timestamp alignment and normalization
Attached files |
MQL5.zip (36.11 KB)
Engineering a Self-Healing Expert Advisor in MQL5 (Part 1): Persistent Trade State Architecture Engineering a Self-Healing Expert Advisor in MQL5 (Part 1): Persistent Trade State Architecture
This article demonstrates how to build the persistence foundation of a self-healing Expert Advisor in MQL5 using SQLite. Readers will learn how to create a permanent trade-state storage layer capable of surviving terminal restarts, shutdowns, and unexpected interruptions. The article covers SQLite integration in MetaTrader 5, database lifecycle management, persistent trade-state structures, and runtime state recovery using practical MQL5 implementations.
Meta-Labeling the Classics (Part 1): Filtering and Sizing RSI Trades Meta-Labeling the Classics (Part 1): Filtering and Sizing RSI Trades
RSI accumulates losses in trending conditions by firing at every threshold crossing regardless of market regime. A Random Forest secondary classifier trained on 12 contextual features — RSI momentum slope, EMA50 trend velocity, ATR-normalised trend stretch, and nine others — filters raw signals and scales position size by classifier confidence on EURUSD H1. Results compare plain RSI, meta-filtered RSI, and bet-sized RSI across a 16-month out-of-sample period with per-trade metrics and drawdown diagnostics.
Detecting and Classifying Fractal Patterns Using Machine Learning Detecting and Classifying Fractal Patterns Using Machine Learning
In this article, we will touch upon the intriguing topic of fractal analysis and market forecasting using machine learning. These are just the first steps towards exploring the diverse fractal structures that form on financial price charts. We will use the correlation to find patterns and the CatBoost algorithm to classify these patterns.
Covariance Matrix Adaptation Evolution Strategy (CMA-ES) Covariance Matrix Adaptation Evolution Strategy (CMA-ES)
The article explores one of the most interesting non-gradient optimization algorithms, which learns to understand the geometry of the objective function. We will focus on the classical implementation of CMA-ES with a slight modification - replacing the normal distribution with the power one. We will thoroughly examine the math behind the algorithm, as well as practical implementation, and check where CMA-ES is unbeatable and where it should be avoided.