preview
From Cloud to Complex: The Vietoris-Rips Filtration in MQL5

From Cloud to Complex: The Vietoris-Rips Filtration in MQL5

MetaTrader 5Trading |
128 0
Hammad Dilber
Hammad Dilber

Contents

  1. Introduction: From a Cloud to a Shape
  2. A Filtration on a Chart
  3. Simplices: the Building Blocks
  4. The Vietoris-Rips Filtration
  5. Implementation: CTDARips
  6. Sorting and O(1) Lookups
  7. The Boundary Matrix over Z/2
  8. Implementation: CTDABoundary
  9. A Worked Example and a Sanity Check
  10. What's Next
  11. Conclusion


Introduction: From a Cloud to a Shape

The previous article produced two concrete geometric objects: a Takens-embedded point cloud and its full pairwise distance matrix. Those are necessary but not sufficient for answering the topological questions we care about (Do clusters exist? Is there a persistent loop?). What we need next is a reproducible, computable topological object that records when simplices appear as we relax a scale parameter ε: the Vietoris–Rips filtration (and the boundary relations that let us compute homology).

Concretely, the task addressed here is implementation-focused: given N embedded points and a cutoff maxEpsilon, enumerate all simplices up to maxDim ≤ 2, assign each simplex its filtration value (vertex = 0, edge = distance between endpoints, triangle = maximum of its three edge lengths), and produce two artifacts suitable for reduction. The implementation must enforce the key invariants that the reduction step requires: simplices are sorted by filtration (ties broken by lower dimension so faces appear before cofaces), vertex and edge lookups provide O(1) mapping to global indices, and each boundary column is stored in ascending order so its pivot can be read without extra work. Practical constraints (edges O(N^2), triangles O(N^3)) motivate capping dimension and using memory-for-speed lookup tables.


A Filtration on a Chart

Three panels of the same point cloud: isolated points, then a ring of edges forming a loop, then the loop filled with triangles

Fig. 1. One point cloud at three scales. At a small epsilon the points are isolated. At a medium epsilon a ring of edges closes a loop. At a large epsilon triangles fill the loop and it disappears.

The figure above is the whole article in one picture. It shows a single point cloud three times, at three values of a scale parameter we call epsilon. Nothing about the points changes between panels. Only the rule for connecting them changes.

On the left, epsilon is small. No two points are within range, so the cloud is just isolated dots. There are twelve connected components and no loops. In the middle, epsilon is larger. Each point now reaches its neighbors, and the edges close into a ring. There is one connected component and one loop. On the right, epsilon is larger still. Points across the ring now connect, triangles fill the interior, and the loop is gone. One connected component, no loops.

The loop in the middle panel is the signal. It was born when the ring closed and it died when the interior filled. The range of epsilon over which it stayed alive is its persistence. A long-lived loop is a real feature of the data. A loop that is born and dies almost immediately is noise. That is what this article builds the machinery to measure. The rest explains how.


Simplices: the Building Blocks

A single dot, two dots joined by a line, and three dots forming a filled triangle

Fig. 2. The three simplex types this library uses: a vertex (dimension 0), an edge (dimension 1), and a filled triangle (dimension 2).

A simplicial complex is built from simple pieces called simplices. A 0-simplex is a single vertex. A 1-simplex is an edge joining two vertices. A 2-simplex is a filled triangle on three vertices. Higher simplices exist (a 3-simplex is a solid tetrahedron) but this library stops at triangles, which is enough to detect connected components and loops.

The dimension of a simplex is one less than its number of vertices. A vertex has one vertex and dimension 0. An edge has two vertices and dimension 1. A triangle has three vertices and dimension 2. The library stores each simplex in a small fixed-size struct.

//+------------------------------------------------------------------+
//| SSimplex - one simplex in the filtration                         |
//|                                                                  |
//|  Stores the vertex set (sorted ascending), the filtration value  |
//|  at which the simplex enters, and its dimension. Up to 3 vertices|
//|  are kept (max dim = 2 = triangle); unused slots hold -1.        |
//+------------------------------------------------------------------+
struct SSimplex
  {
   double   filtration;     // filtration value at which simplex appears
   int      dim;            // 0 = vertex, 1 = edge, 2 = triangle
   int      v[3];           // sorted vertex indices, unused = -1

   void     Reset()
     {
      filtration = 0.0;
      dim        = 0;
      v[0]       = -1;
      v[1]       = -1;
      v[2]       = -1;
     }
  };

The struct holds three things: the filtration value at which the simplex appears, its dimension, and its vertex set. The vertex array has room for three indices because a triangle is the largest simplex we build. A vertex uses one slot and leaves the other two at -1. An edge uses two. The vertices are stored in ascending order, which matters later when we look an edge up by its endpoints.

The filtration value is the key field. For a vertex it is 0, because every point is present from the very start. For an edge it is the distance between its two endpoints, because that is the smallest epsilon at which the edge appears. For a triangle it is the largest of its three edge lengths, because all three edges must be present before the triangle can form. That single rule, filtration equals the longest edge, is the heart of the Vietoris-Rips construction.


The Vietoris-Rips Filtration

The Vietoris-Rips rule is mechanical. Fix a scale epsilon. Then:

An edge (i,j) exists if distance(i,j) <= epsilon. A triangle (i,j,k) exists if all three of its edges exist.

As epsilon grows from 0 upward, simplices only ever get added, never removed. A small epsilon gives a sparse complex of isolated vertices. A large epsilon gives a dense complex where every triple is a filled triangle. The filtration is the entire ordered sequence of simplices, each tagged with the epsilon at which it enters.

Recording a continuous range of epsilon values would be impossible. The insight that makes it finite is that the complex only changes at specific values: the moment a new edge or triangle appears. Those moments are exactly the simplex filtration values. So instead of stepping epsilon through a continuum, we build every simplex once, tag it with its entry value, and sort the whole set by that value. Reading the sorted list from start to finish replays the filtration exactly.

The sort order has one subtlety. When two simplices share the same filtration value, the lower-dimensional one must come first. An edge cannot appear before its endpoints, and a triangle cannot appear before its edges. Breaking ties by dimension guarantees that every simplex appears after its faces, which the boundary matrix later depends on.

The cost of this construction is worth stating plainly. For N points there are up to N(N-1)/2 edges and N(N-1)(N-2)/6 triangles, so the triangle count grows cubically in N. With a window of 80 bars the cloud has 78 points, and a full complex reaches tens of thousands of triangles. This cubic growth is why the fourth article caps the window size and why the reduction step in the next article is the most expensive part of the pipeline.


Implementation: CTDARips

The CTDARips class builds the filtration from a distance matrix. It stores the simplices in a single dynamic array, tracks counts per dimension, and exposes lookups that the boundary builder will need.

//+------------------------------------------------------------------+
//| CTDARips - builds the Vietoris-Rips filtration up to maxDim <= 2 |
//+------------------------------------------------------------------+
class CTDARips
  {
private:
   SSimplex          m_simplices[];     // sorted filtration order
   int               m_count;           // total simplex count (logical size)
   int               m_capacity;        // allocated capacity of m_simplices
   int               m_N;               // number of points
   int               m_maxDim;          // max dim built (<= 2)
   double            m_maxFiltration;   // max filtration value in complex

   int               m_vertexIdx[];     // [N]    : global idx of vertex {v}
   int               m_edgeIdx[];       // [N*N]  : global idx of edge {i,j}
   int               m_dimCount[3];     // simplex count per dim

   //--- helpers
   void              SwapSimplex(int i, int j);
   bool              SimplexLess(int a, int b) const;
   void              QuickSort(int low, int high);
   void              BuildLookups();
   void              EnsureCapacity(int needed);
   void              AppendSimplex(double f, int dim, int v0, int v1, int v2);

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

   bool              Build(const CTDADistance &dist,
                           double maxEpsilon,
                           int    maxDim = 2);

   int               Size()          const { return m_count; }
   int               SizeByDim(int d) const;
   double            MaxFiltration() const { return m_maxFiltration; }
   bool              Get(int idx, SSimplex &out) const;

   int               VertexGlobalIdx(int v)      const;
   int               EdgeGlobalIdx(int i, int j) const;
  };

The simplices live in one flat array, m_simplices, grown with the same amortized capacity-doubling pattern introduced in the previous article. Enumerating simplices one at a time and calling a grow-by-one resize on each would be quadratic. Doubling the capacity keeps each append amortized O(1). The two lookup arrays, m_vertexIdx and m_edgeIdx, are filled after the sort and are explained in the next section.

The Build method is where the filtration is assembled. It runs in three passes: vertices, then edges, then triangles.

//+------------------------------------------------------------------+
//| Build the full Vietoris-Rips filtration                          |
//+------------------------------------------------------------------+
bool CTDARips::Build(const CTDADistance &dist, double maxEpsilon, int maxDim)
  {
   m_N      = dist.Size();
   m_maxDim = (maxDim > 2 ? 2 : (maxDim < 0 ? 0 : maxDim));

   if(m_N < 2)
     {
      Print("TDARips::Build - need at least 2 points");
      return false;
     }
   if(maxEpsilon <= 0.0)
     {
      Print("TDARips::Build - maxEpsilon must be > 0");
      return false;
     }

   //--- reset logical size, keep allocated capacity for reuse
   m_count            = 0;
   m_maxFiltration    = 0.0;
   m_dimCount[0]      = 0;
   m_dimCount[1]      = 0;
   m_dimCount[2]      = 0;

   //--- 0-simplices: every vertex, filtration 0
   for(int i = 0; i < m_N; i++)
      AppendSimplex(0.00, i, -1, -1);

   if(m_maxDim < 1)
     {
      QuickSort(0, m_count - 1);
      BuildLookups();
      return true;
     }

   //--- 1-simplices: edges with d <= maxEpsilon
   for(int i = 0; i < m_N; i++)
      for(int j = i + 1; j < m_N; j++)
        {
         double d = dist.Get(i, j);
         if(d > maxEpsilon) continue;
         AppendSimplex(d, 1, i, j, -1);
        }

   if(m_maxDim < 2)
     {
      QuickSort(0, m_count - 1);
      BuildLookups();
      return true;
     }

   //--- 2-simplices: triangles with all three edges within maxEpsilon
   for(int i = 0; i < m_N; i++)
      for(int j = i + 1; j < m_N; j++)
        {
         double dij = dist.Get(i, j);
         if(dij > maxEpsilon) continue;
         for(int k = j + 1; k < m_N; k++)
           {
            double dik = dist.Get(i, k);
            if(dik > maxEpsilon) continue;
            double djk = dist.Get(j, k);
            if(djk > maxEpsilon) continue;

            double f = MathMax(dij, MathMax(dik, djk));
            AppendSimplex(f, 2, i, j, k);
           }
        }

   //--- sort the entire filtration in ascending order
   QuickSort(0, m_count - 1);
   BuildLookups();
   return true;
  }

The method first clamps the maximum dimension to 0–2 and validates the inputs. It then resets the logical size while reusing the previously allocated capacity. After that, it runs three passes.

The first pass adds every vertex at filtration 0. If the caller asked for dimension 0 only, the method sorts and returns. The second pass walks every pair (i,j) with j greater than i, reads their distance, and adds an edge when the distance is within maxEpsilon. The edge filtration is the distance itself. The third pass walks every triple (i,j,k), checks that all three edges are within maxEpsilon, and adds a triangle whose filtration is the largest of the three edge lengths. The early continue statements skip triples as soon as one edge is too long, which avoids reading the third distance when the first already fails.

After the passes, a single QuickSort orders the entire array by filtration and then by dimension. The final BuildLookups call indexes the sorted vertices and edges. When Build returns, the object holds the complete filtration in replay order.


Sorting and O(1) Lookups

The sort comparator encodes the tie-breaking rule from the theory section: filtration ascending, dimension ascending on ties.

//+------------------------------------------------------------------+
//| Sorting order: filtration ascending, ties broken by dim ascending|
//+------------------------------------------------------------------+
bool CTDARips::SimplexLess(int a, int b) const
  {
   if(m_simplices[a].filtration < m_simplices[b].filtration) return true;
   if(m_simplices[a].filtration > m_simplices[b].filtration) return false;
   return (m_simplices[a].dim < m_simplices[b].dim);
  }

A simplex with a smaller filtration comes first. When two filtration values are equal, the one with the smaller dimension comes first. This is the property that guarantees faces appear before the simplices they bound. The library sorts with an in-place quicksort rather than a built-in, because the comparator is a three-field comparison and the array holds structs by value.

After sorting, the position of each simplex in the array is its global index, and that index is how every other part of the pipeline refers to it. The boundary builder, given a triangle, needs to answer a specific question fast: what is the global index of the edge between vertices i and j? Scanning the whole array for that edge would make the boundary construction quadratic. The lookup tables avoid it.

//+------------------------------------------------------------------+
//| After sort, build vertex/edge -> global-index lookup tables      |
//+------------------------------------------------------------------+
void CTDARips::BuildLookups()
  {
   ArrayResize(m_vertexIdx, m_N);
   for(int v = 0; v < m_N; v++) m_vertexIdx[v] = -1;

   ArrayResize(m_edgeIdx, m_N * m_N);
   ArrayInitialize(m_edgeIdx, -1);

   for(int s = 0; s < m_count; s++)
     {
      const int dim = m_simplices[s].dim;
      if(dim == 0)
        {
         m_vertexIdx[m_simplices[s].v[0]] = s;
        }
      else if(dim == 1)
        {
         int a = m_simplices[s].v[0];
         int b = m_simplices[s].v[1];
         m_edgeIdx[a * m_N + b] = s;
         m_edgeIdx[b * m_N + a] = s;     // symmetric
        }
     }
  }

BuildLookups fills two arrays. The vertex table maps a point index directly to the global index of its vertex simplex. The edge table is an N by N grid that maps a pair of point indices to the global index of their edge, or -1 if that edge is not in the complex. Both directions (i,j) and (j,i) are written, so a caller can look an edge up without sorting its endpoints first. With these tables, the boundary builder resolves any face in constant time.

The edge table uses N² integers. For the window sizes used in trading, that is a few thousand entries and it turns a quadratic scan into a constant-time lookup. The same trade, memory for speed, recurs throughout the library.


The Boundary Matrix over Z/2

A 7 by 7 grid showing which lower simplices are faces of each simplex

Fig. 3. The boundary matrix over Z/2 of a single filled triangle. Each column lists the faces of one simplex: edges point to their two vertices, the triangle points to its three edges.

The filtration tells us when each simplex appears. The boundary matrix tells us how simplices fit together. The boundary of a simplex is the set of simplices one dimension lower that form its border. The boundary of an edge is its two endpoint vertices. The boundary of a triangle is its three edges. The boundary of a vertex is empty.

We work over the two-element field Z/2, which means coefficients are either 0 or 1 and arithmetic is modulo 2. There are no signs and no orientations to track, which is exactly what makes the matrix simple to store and fast to reduce. A column either contains a given face or it does not. The figure above shows the full matrix for one filled triangle: three empty vertex columns, three edge columns each pointing to two vertices, and one triangle column pointing to three edges.

Working over Z/2 also gives column addition a clean meaning. Adding two columns is the symmetric difference of their face sets: a face that appears in both cancels, a face that appears in one survives. For example, adding the boundary of edge (v0,v1) to the boundary of edge (v1,v2) gives:

{v0, v1} + {v1, v2} = {v0, v2}

The shared vertex v1 appears twice and cancels modulo 2, leaving the two outer vertices. This symmetric-difference addition is the single operation the reduction algorithm repeats thousands of times in the next article, so the boundary is stored in a form that makes it cheap.


Implementation: CTDABoundary

A full N by N boundary matrix would be almost entirely zeros: a triangle touches only three edges out of thousands. CTDABoundary stores it sparsely, as a flat list of face indices plus per-column offsets.

//+------------------------------------------------------------------+
//| CTDABoundary - sparse column form of the boundary over Z/2       |
//|                                                                  |
//|  Column j stores the global indices of the (k-1)-faces of the    |
//|  k-simplex at position j in the sorted filtration:               |
//|    - 0-simplex (vertex)    : empty column                        |
//|    - 1-simplex (edge ij)   : {vertex(i), vertex(j)}              |
//|    - 2-simplex (tri ijk)   : {edge(ij), edge(ik), edge(jk)}      |
//+------------------------------------------------------------------+
class CTDABoundary
  {
private:
   int               m_entries[];      // flat face-index storage
   int               m_colStart[];     // start offset of column j
   int               m_colSize[];      // size of column j
   int               m_numCols;        // = number of simplices
   int               m_totalEntries;   // logical size used in m_entries
   int               m_entriesCap;     // allocated capacity of m_entries

   void              Sort3Asc(int &a, int &b, int &c) const;
   void              ReserveEntries(int needed);

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

   bool              Build(const CTDARips &rips);

   int               NumCols()      const { return m_numCols; }
   int               ColSize(int j) const;
   int               Entry(int j, int k) const;
   void              GetColumn(int j, int &out[]) const;
   int               TotalEntries() const { return m_totalEntries; }
  };

Three arrays carry the data. The m_entries array is one long buffer holding the face indices of every column back to back. The m_colStart array records where each column begins inside that buffer, and m_colSize records how many entries it has. Reading column j means reading m_colSize[j] entries starting at m_colStart[j]. This packs the whole sparse matrix into three arrays with no wasted zeros.

The Build method walks the sorted filtration once and writes one column per simplex.

//+------------------------------------------------------------------+
//| Build the boundary column-by-column over the sorted filtration   |
//+------------------------------------------------------------------+
bool CTDABoundary::Build(const CTDARips &rips)
  {
   m_numCols = rips.Size();
   if(m_numCols <= 0)
     {
      Print("TDABoundary::Build - empty filtration");
      return false;
     }

   ArrayResize(m_colStart, m_numCols);
   ArrayResize(m_colSize,  m_numCols);
   m_totalEntries = 0;
   ReserveEntries(3 * m_numCols);

   for(int j = 0; j < m_numCols; j++)
     {
      SSimplex s;
      rips.Get(j, s);
      m_colStart[j] = m_totalEntries;

      if(s.dim == 0)
        {
         m_colSize[j] = 0;          // vertices have empty boundary
         continue;
        }

      if(s.dim == 1)
        {
         // boundary of edge {i,j} = vertex i + vertex j
         int va = rips.VertexGlobalIdx(s.v[0]);
         int vb = rips.VertexGlobalIdx(s.v[1]);
         int lo = (va < vb ? va : vb);
         int hi = (va < vb ? vb : va);

         m_entries[m_totalEntries]     = lo;
         m_entries[m_totalEntries + 1] = hi;
         m_totalEntries += 2;
         m_colSize[j] = 2;
         continue;
        }

      // boundary of triangle {i,j,k} = edge(i,j) + edge(i,k) + edge(j,k)
      int eab = rips.EdgeGlobalIdx(s.v[0], s.v[1]);
      int eac = rips.EdgeGlobalIdx(s.v[0], s.v[2]);
      int ebc = rips.EdgeGlobalIdx(s.v[1], s.v[2]);
      Sort3Asc(eab, eac, ebc);

      m_entries[m_totalEntries]     = eab;
      m_entries[m_totalEntries + 1] = eac;
      m_entries[m_totalEntries + 2] = ebc;
      m_totalEntries += 3;
      m_colSize[j] = 3;
     }
   return true;
  }

For each simplex the method records the column start, then branches on dimension. A vertex writes an empty column. An edge resolves its two endpoint vertices through the vertex lookup, stores them in ascending order, and records a column of size two. A triangle resolves its three edges through the edge lookup, sorts the three global indices ascending, and records a column of size three.

The ascending sort on each column is deliberate. The reduction algorithm in the next article reads the pivot of a column as its last entry, so every column is kept sorted at build time to make that read free. The buffer is pre-reserved to three entries per column, the maximum any column needs, so the loop never resizes mid-flight. When Build returns, the boundary is complete and read-only.


A Worked Example and a Sanity Check

A tiny example makes the loop concrete. Take four points at the corners of a square and choose an epsilon long enough to connect the four sides but too short to reach across the diagonals.

Two panels: a four-edge square with an empty center forming a loop, and the same square with a diagonal splitting it into two filled triangles

Fig. 4. Left: four edges close a loop around an empty center,  a one-dimensional hole (H1). Right: once a diagonal is short enough to enter, two triangles fill the square and the loop dies.

The four sides become edges. The two diagonals are longer than epsilon, so they are absent, and with no diagonal there is no triangle. The result is a closed ring of four edges around an empty center. That empty center is a one-dimensional hole, an H1 feature, born the moment the fourth edge closed the ring. If epsilon grew until a diagonal appeared, two triangles would fill the square and the hole would die. This is the same birth-and-death story as the middle panel of the opening figure, reduced to four points you can check by hand.

The library reproduces this at scale. The attached script TDA_Rips_Demo.mq5 pulls a window of closes off the current chart, runs the full pipeline through the boundary matrix, and prints a summary of each stage.

#include <TDA/TDAPointCloud.mqh>
#include <TDA/TDADistance.mqh>
#include <TDA/TDARips.mqh>
#include <TDA/TDABoundary.mqh>

input int    InpWindow     = 80;    // bars of close history to embed
input int    InpEmbDim     = 3;     // embedding dimension d
input int    InpDelay      = 1;     // time delay tau
input double InpMaxEpsFrac = 1.0;   // VR cutoff = frac * max pairwise distance
input int    InpMaxDim     = 2;     // max simplex dim (1 = edges, 2 = triangles)

void OnStart()
  {
   double closes[];
   int copied = CopyClose(_Symbol_Period0, InpWindow, closes);
   if(copied != InpWindow)
      return;

   //--- point cloud and distance matrix
   CTDAPointCloud cloud;
   if(!cloud.Build(closes, InpWindow, InpEmbDim, InpDelay))
      return;

   CTDADistance dist;
   if(!dist.Build(cloud, TDA_NORM_EUCLIDEAN))
      return;

   //--- Vietoris-Rips filtration up to maxDim
   double maxEps = InpMaxEpsFrac * dist.MaxDistance();
   CTDARips rips;
   if(!rips.Build(dist, maxEps, InpMaxDim))
      return;
   rips.PrintSummary();

   //--- boundary matrix over Z/2
   CTDABoundary boundary;
   if(!boundary.Build(rips))
      return;
   boundary.PrintSummary();
  }

Run on an 80-bar GBPUSD H1 window with the default cutoff, the script reports the following.

Stage
Quantity
Value
Point cloud
Points (N)
78
Filtration
Vertices
78
Filtration
Edges
3003
Filtration
Triangles
76076
Filtration
Total simplices
79157
Filtration
Max filtration
0.014544
Boundary
Non-zero entries
234234

These numbers are a sanity check in themselves. With the default cutoff set to the full diameter of the cloud, every pair of points is within range, so every pair is an edge and every triple is a triangle. The edge count 3003 is exactly C(78,2), the number of ways to choose two points from 78. The triangle count 76076 is exactly C(78,3). The boundary entry count 234234 is 3003 edges times two faces plus 76076 triangles times three faces. The counts match the combinatorics to the integer, which is the simplest possible confirmation that the enumeration is complete and correct.

The shape behaves correctly too. A cloud sampled from a noisy circle, run through this pipeline, produces a complex whose middle scales contain exactly one persistent ring, just like the opening figure. The full numerical validation of birth and death values waits for the next article, where the reduction step turns this scaffolding into an actual persistence diagram. For now the filtration and its boundary are built, ordered, and verified by counting.


What's Next

We have a filtration and a boundary matrix. We do not yet have persistence. The boundary matrix encodes which simplices bound which, but the loops and components are still implicit in it. The next article extracts them.

  • Homology dimensions: H0 for connected components, H1 for loops, H2 for voids.
  • The standard reduction algorithm: column addition over Z/2 until each column has a unique pivot.
  • CTDAReduction, which turns the boundary matrix into a list of birth-death persistence pairs.
  • CTDADiagram and the CTDA facade, plus the full numerical verification of the library.


Conclusion

This article turns distances into the formal topological scaffolding required for persistent homology and produces two verified, ready-to-use artifacts.

What was built

  • CTDARips: enumerates vertices, edges, and triangles up to dim ≤ 2, tags each simplex with its filtration value, sorts the entire list by (filtration ascending, dimension ascending), and builds O(1) lookup tables mapping (vertex) → global index and (i,j) → edge global index. The global index of a simplex is its position in the sorted filtration array.
  • CTDABoundary: consumes the sorted filtration and writes the sparse boundary matrix over Z/2 in column form. Each column stores the global indices of the faces (vertex columns empty, edge columns hold two vertex indices, triangle columns hold three edge indices). Columns are stored ascending so the column pivot is the last entry without extra sorting.

Invariants and verification

  • Faces always appear earlier than cofaces (tie-break by dimension), guaranteeing that face indices in boundary columns are smaller than the column index.
  • Columns are pre-sorted, enabling the reduction routine to read pivots in O(1).
  • Memory/speed tradeoffs (N² edge lookup table, flat entries buffer) keep boundary construction fast at the cost of modest extra memory.
  • A sanity check on a real run (N = 78) produced exact combinatorial counts (edges = C(78,2), triangles = C(78,3), and total non-zero boundary entries = 2 #edges + 3 #triangles), confirming completeness and correct indexing.

With the filtration and sparse boundary matrix in place and the invariants enforced, the pipeline is one reduction step away from producing persistence pairs. The next article will implement CTDAReduction, produce persistence diagrams, and include numerical verification of birth–death values.

#
Filename
Type
Description
1
TDAPointCloud.mqh
Header
Takens phase-space embedding (from the previous article)
2
TDADistance.mqh
Header
Pairwise distance matrix (from the previous article)
3
TDARips.mqh
Header
Vietoris-Rips filtration (introduced this article)
4
TDABoundary.mqh
Header
Sparse boundary matrix over Z/2 (introduced this article)
5
TDA_Rips_Demo.mq5
Script
Runnable demo: builds the filtration and boundary, prints a summary
Attached files |
MQL5.zip (8.96 KB)
Features of Custom Indicators Creation Features of Custom Indicators Creation
Creation of Custom Indicators in the MetaTrader trading system has a number of features.
Interactive Supply and Demand Zone Manager in MQL5 (Part II): Event-Driven Architecture and Persistent Lifecycle Logging Interactive Supply and Demand Zone Manager in MQL5 (Part II): Event-Driven Architecture and Persistent Lifecycle Logging
This article advances the stateful supply and demand zone framework for MetaTrader 5 by replacing polling with an event-driven model based on OnChartEvent(). We split synchronization into dedicated handlers for creation, modification, and deletion, and separate market logic in OnTick() from user interactions in OnChartEvent(). A persistent, append-only CSV logger records all lifecycle events, improving responsiveness, state consistency, and recoverable history for downstream analysis.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Artificial Atom Algorithm (A3) Artificial Atom Algorithm (A3)
The article describes implementation of the A3 algorithm - a metaheuristic optimization method inspired by chemical processes - in MQL5. Only two adjustable parameters, compactness and a small population, ensure high operating speed with sufficient quality of solutions.