Financial Market Synchronisation — Kuramoto Regime Detection¶
The market module applies Kuramoto synchronisation analysis to financial
time series. It extracts instantaneous phase from asset prices via the
Hilbert transform, computes the time-varying order parameter \(R(t)\) across
assets, and classifies market regimes. Empirical evidence shows that
\(R(t) \to 1\) precedes market crashes — all assets moving in lockstep is
a danger signal, not a sign of health.
This module bridges physics (Kuramoto theory) and quantitative finance (systemic risk measurement), providing a non-parametric, model-free approach to detecting herd behaviour.
1. Mathematical Formalism¶
1.1 Phase Extraction via Hilbert Transform¶
Given a real-valued time series \(x(t)\) (log returns, normalised prices, etc.), the analytic signal is:
where \(\mathcal{H}\) is the Hilbert transform. The instantaneous phase is:
For a stationary oscillation \(x(t) = A\cos(\omega t + \phi_0)\), the Hilbert transform yields exact instantaneous frequency and phase. For non-stationary financial data, the phase captures the "cycle position" of each asset's return dynamics.
Implementation: scipy.signal.hilbert() (FFT-based, \(O(T \log T)\)).
1.2 Market Order Parameter¶
Given \(N\) assets with phases \(\phi_i(t)\), the market-wide Kuramoto order parameter at time \(t\) is:
This is computed independently at each timestep (no rolling window).
| \(R(t)\) | Market state | Interpretation |
|---|---|---|
| \(R < 0.3\) | Desynchronised | Normal heterogeneous trading |
| \(0.3 \leq R < 0.7\) | Transitional | Increasing correlation, elevated risk |
| \(R \geq 0.7\) | Synchronised | Herd behaviour — crash risk elevated |
1.3 Windowed Phase-Locking Value¶
The PLV matrix captures pairwise synchronisation between assets over a rolling window of \(W\) timesteps:
This produces a time-varying \(N \times N\) connectivity matrix. High PLV between two assets indicates consistent phase relationship (they move together or exactly opposite).
Output shape: \((T - W + 1, N, N)\) — one PLV matrix per window position.
Optional Rust, Go, Julia, and Mojo backend returns are validated at the public
dispatcher before they become market evidence. market_order_parameter() only
publishes finite unit-interval R(t) vectors with length T; market_plv()
only publishes flattened backend payloads that reshape to
\((T-W+1) \times N \times N\) PLV matrices with unit diagonals, symmetry, and
unit-interval entries. Loader or runtime unavailability still falls through to
the next backend, but malformed backend physics payloads fail closed.
All public market array boundaries are typed before conversion:
extract_phase() rejects non-real or numeric-string price/return series,
market_order_parameter() and market_plv() reject those aliases in phase
matrices, and detect_regimes() plus sync_warning() reject them in R(t).
The Rust wrappers preserve raw kernel-return dtype until the shared output
validators run; numeric-string returns therefore cannot be laundered into
market evidence by NumPy coercion. Mojo stdout remains an explicit text
protocol with its own strict parser.
1.4 Regime Classification¶
The detect_regimes function maps \(R(t)\) to discrete labels:
| Label | Value | Condition | Meaning |
|---|---|---|---|
| DESYNC | 0 | \(R \leq R_{\text{desync}}\) | Normal market |
| TRANSITION | 1 | \(R_{\text{desync}} < R < R_{\text{sync}}\) | Elevated risk |
| SYNC | 2 | \(R \geq R_{\text{sync}}\) | Herd/crash risk |
Default thresholds: \(R_{\text{sync}} = 0.7\), \(R_{\text{desync}} = 0.3\).
1.5 Synchronisation Warning Signal¶
The sync_warning function detects upward crossings of \(R(t)\) through
a threshold (default 0.7). It optionally applies a moving-average smoother
(lookback parameter) to reduce false alarms from noise:
Warning fires at time \(t\) when \(\bar{R}(t) \geq \theta\) and \(\bar{R}(t-1) < \theta\).
1.6 Empirical Evidence¶
| Event | \(R\) before crash | Lead time | Source |
|---|---|---|---|
| Black Monday 1987 | \(R > 0.8\) | ~2 weeks | Harmon et al. 2011 |
| Asian Crisis 1997 | \(R > 0.7\) | ~1 month | Münnix et al. 2012 |
| GFC 2008 | \(R > 0.85\) | ~3 months | Kenett et al. 2012 |
| COVID crash 2020 | \(R > 0.9\) | ~1 week | (rapid onset) |
The mechanism: as market stress increases, correlations rise, diversification breaks down, and the portfolio of asset phases collapses toward a single direction — the Kuramoto phase transition.
2. Theoretical Context¶
2.1 Historical Background¶
The application of Kuramoto theory to financial markets was pioneered by Münnix et al. (2012) who showed that the cross-correlation matrix of stock returns behaves like a system of coupled oscillators near a phase transition. The order parameter \(R\) captures the dominant eigenvalue behaviour of the correlation matrix in a single scalar.
Harmon et al. (2011) independently used a "social synchrony index" based on similar principles to predict the 2008 crash with months of lead time.
The CEUR-WS Vol-915 paper cited in the module docstring connects this analysis to the broader Kuramoto framework, enabling the full SPO toolkit (bifurcation analysis, basin stability, chimera detection) to be applied to financial data.
2.2 Role in SCPN¶
The market module is a standalone analytical tool — it does not participate in the real-time integration pipeline. It is used for:
- Systemic risk monitoring — real-time \(R(t)\) computation for portfolio risk dashboards
- Regime-conditional strategies — different trading/hedging logic in SYNC vs DESYNC regimes
- Research — studying market microstructure through the lens of phase synchronisation
2.3 Connection to Random Matrix Theory¶
For \(N\) assets with i.i.d. returns, the expected \(R\) from random phases scales as \(R_{\text{random}} \sim 1/\sqrt{N}\). The Marchenko-Pastur distribution from Random Matrix Theory (RMT) predicts the distribution of eigenvalues of the sample correlation matrix. The order parameter \(R\) is related to the largest eigenvalue \(\lambda_1\):
When \(\lambda_1\) significantly exceeds the RMT upper bound \(\lambda_+ = (1 + \sqrt{N/T})^2\), the market is in a synchronised state.
2.4 Systemic Risk Indicators¶
The market order parameter is one of several systemic risk measures. Comparison with alternatives:
| Measure | Type | Captures | Computational cost |
|---|---|---|---|
| Kuramoto \(R(t)\) | Phase coherence | Phase alignment | \(O(TN)\) |
| Absorption ratio | Eigenvalue | Variance concentration | \(O(TN^2)\) |
| SRISK | Balance-sheet | Capital shortfall | Requires accounting data |
| CoVaR | Tail dependence | Conditional tail risk | \(O(TN^2)\) quantile regression |
| Granger causality | Directional | Causal linkages | \(O(TN^2)\) VAR estimation |
Advantages of \(R(t)\): - Model-free (no assumed return distribution) - Real-time computable (no window-end effects) - Single scalar per timestep (easy to monitor) - Grounded in physics (Kuramoto theory provides interpretation)
Disadvantages: - Ignores return magnitude (only phase matters) - No directional information - Hilbert phase is noisy for broadband signals
2.5 Pre-Processing Recommendations¶
For best phase extraction quality:
- Detrend: Remove linear or polynomial trend before Hilbert
- Band-pass filter: Extract specific frequency band (e.g., 20-60 day cycle for business cycle component)
- Normalise: Z-score each asset's returns to equalise scale
- Pad: Hilbert suffers edge effects — pad with reflection or zeros
The module does NOT pre-process automatically. This is deliberate — the user should choose filtering appropriate to their frequency of interest.
2.6 Limitations¶
- Hilbert transform artefacts: The Hilbert transform assumes a narrow-band signal. Broadband financial data (white noise + trends) produces noisy phase estimates. Pre-filtering (band-pass) is recommended for high-quality phase extraction.
- Non-stationarity: Financial time series are non-stationary. The phase extraction and \(R(t)\) computation assume local stationarity within each window.
- Threshold sensitivity: The warning system depends on fixed thresholds. Adaptive thresholds (e.g., percentile-based) may perform better across different market regimes.
- No directional information: \(R(t)\) measures coherence but not direction. A crash (all assets falling together) and a bubble (all assets rising together) both produce high \(R\).
3. Pipeline Position¶
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ Price data │────→│ extract_phase() │────→│ phases (T,N) │
│ (T, N) │ │ Hilbert transform│ └──────┬───────┘
└──────────────┘ └──────────────────┘ │
┌────▼────────┐
│ market_ │
│ order_ │
│ parameter() │
│ → R(t) │
└────┬────────┘
│
┌──────────────────┐ ┌──────▼───────┐
│ market_plv() │ │ detect_ │
│ → PLV(t,N,N) │ │ regimes() │
│ pairwise sync │ │ → labels (T) │
└──────────────────┘ └──────┬───────┘
│
┌──────▼───────┐
│ sync_warning()│
│ → bool (T) │
└──────────────┘
Inputs: Price/return time series (T, N) — T timesteps, N assets Outputs: \(R(t)\), PLV matrices, regime labels, warning signals
4. Features¶
4.1 Four Functions¶
| Function | Purpose | Input | Output |
|---|---|---|---|
extract_phase |
Hilbert phase extraction | (T,) or (T,N) |
Same shape, \([0, 2\pi)\) |
market_order_parameter |
\(R(t)\) across assets | (T, N) phases |
(T,) in \([0, 1]\) |
market_plv |
Windowed PLV matrix | (T, N) + window |
(T-W+1, N, N) |
detect_regimes |
Regime classification | (T,) R values |
(T,) int labels |
sync_warning |
Threshold crossing | (T,) R values |
(T,) bool |
4.2 Rust Acceleration¶
Direct Mojo market adapters enforce exact raw stdout cardinality: ORDER must
emit one finite scalar per timestep, and PLV must emit one finite scalar per
rolling-window matrix entry. Blank, truncated, overlong, non-numeric, or
non-finite output is rejected before public arrays are returned.
market_order_parameter, market_plv, and detect_regimes all have Rust
backends via spo-kernel. Speedup is significant for market_plv which
is \(O(T \cdot W \cdot N^2)\).
4.3 Vectorised Implementation¶
The Python fallback uses fully vectorised NumPy operations — no Python
loops except in sync_warning (sequential threshold crossing detection).
5. Usage Examples¶
5.1 Basic Market Analysis¶
import numpy as np
from scpn_phase_orchestrator.upde.market import (
extract_phase, market_order_parameter, detect_regimes, sync_warning,
)
# Simulate 5 assets over 500 timesteps
rng = np.random.default_rng(42)
T, N = 500, 5
returns = rng.normal(0, 0.01, (T, N))
# Add correlation spike (simulated crash approach)
returns[300:, :] += 0.005 * np.sin(np.arange(200)[:, None] * 0.1)
phases = extract_phase(returns)
R = market_order_parameter(phases)
regimes = detect_regimes(R, sync_threshold=0.7, desync_threshold=0.3)
warnings = sync_warning(R, threshold=0.7, lookback=10)
print(f"Mean R: {R.mean():.3f}")
print(f"Max R: {R.max():.3f}")
print(f"Warnings fired: {warnings.sum()}")
print(f"Regime counts: desync={np.sum(regimes==0)}, "
f"transition={np.sum(regimes==1)}, sync={np.sum(regimes==2)}")
5.2 PLV Connectivity Matrix¶
from scpn_phase_orchestrator.upde.market import market_plv
plv = market_plv(phases, window=50)
# plv shape: (451, 5, 5)
# Average PLV over all windows
mean_plv = plv.mean(axis=0)
print("Mean PLV matrix:")
print(np.round(mean_plv, 2))
# High off-diagonal values = persistent co-movement
5.3 Real Data (CSV)¶
import pandas as pd
# prices = pd.read_csv("sp500_daily.csv", index_col=0, parse_dates=True)
# returns = prices.pct_change().dropna().values # (T, N)
# phases = extract_phase(returns)
# R = market_order_parameter(phases)
# Uncomment above for real data
5.4 Crash Precursor Detection¶
# Detect when R crosses 0.7 — potential crash precursor
warnings = sync_warning(R, threshold=0.7, lookback=20)
warning_times = np.where(warnings)[0]
for t in warning_times:
print(f"⚠ Sync warning at t={t}, R={R[t]:.3f}")
5.5 Regime Duration Analysis¶
regimes = detect_regimes(R)
# Calculate mean duration of each regime
changes = np.diff(regimes)
change_points = np.where(changes != 0)[0]
durations = np.diff(np.concatenate([[0], change_points, [len(regimes)]]))
print(f"Mean regime duration: {durations.mean():.1f} timesteps")
5.6 Multi-Window PLV Comparison¶
# Compare PLV at different timescales
for w in [10, 50, 200]:
plv_w = market_plv(phases, window=w)
mean_plv_w = np.mean(plv_w)
print(f"Window={w}: mean PLV={mean_plv_w:.3f}")
# Longer windows smooth out noise, shorter windows are more responsive
5.7 Risk Dashboard Integration¶
# Real-time monitoring loop (pseudo-code)
def update_risk_dashboard(new_returns_row):
"""Called once per timestep with new asset returns."""
# Append to rolling buffer
buffer.append(new_returns_row)
if len(buffer) < 100:
return # need minimum history for Hilbert
phases = extract_phase(np.array(buffer[-200:]))
R_current = float(market_order_parameter(phases)[-1])
if R_current > 0.7:
alert("SYNC WARNING", level="high", R=R_current)
elif R_current > 0.5:
alert("TRANSITION", level="medium", R=R_current)
5.8 Bifurcation Diagram of Market K_c¶
from scpn_phase_orchestrator.upde.bifurcation import trace_sync_transition
# Treat market as Kuramoto system: estimate K_c from return data
# omegas = mean return rates, knm = correlation-derived coupling
correlation = np.corrcoef(returns.T) # N×N
omegas_market = np.mean(returns, axis=0) * 252 # annualised
# Use correlation as coupling proxy
knm_market = np.maximum(correlation, 0) / N
np.fill_diagonal(knm_market, 0.0)
diagram = trace_sync_transition(
omegas_market,
knm_template=knm_market,
K_range=(0.0, 10.0),
n_points=30,
)
print(f"Market-implied K_c = {diagram.K_critical}")
5.9 Sector Decomposition¶
# Analyse sync within sectors vs across sectors
sectors = {
"tech": [0, 1, 2],
"finance": [3, 4],
"energy": [5, 6, 7],
}
for name, indices in sectors.items():
sector_phases = phases[:, indices]
R_sector = market_order_parameter(sector_phases)
print(f"{name}: mean R = {R_sector.mean():.3f}")
# Sectors synchronise internally before cross-sector sync → cascade
6. Technical Reference¶
6.1 Module API¶
market ¶
Kuramoto-based financial market synchronisation analysis.
Exposes a 5-backend fallback chain.
Extracts instantaneous phase from price / return time series via the
Hilbert transform (scipy.signal.hilbert — FFT-based, stays
Python-side because the Rust/Go/Mojo backends do not ship an FFT),
then dispatches the two post-processing compute kernels:
market_order_parameter(phases)—R(t) = |⟨exp(iθ)⟩_N|at every timestep.O(T · N).market_plv(phases, window)— rolling phase-locking-value matrix between assets,O((T − W + 1) · N² · W)with a sincos precompute that eliminates trig from the inner loop.
The detect_regimes classifier and sync_warning crossing
detector are O(T) masking / comparison operations; they stay pure
NumPy. R(t) → 1 preceded Black Monday 1987 and the 2008
crash (arXiv:1109.1167; CEUR-WS Vol-915).
Functions:¶
extract_phase ¶
Extract instantaneous phase from a time series via the Hilbert transform.
Stays Python-side because the transform is FFT-based
(scipy.signal.hilbert) and the compiled backends do not
ship an FFT library.
Parameters¶
series : FloatArray
Real-valued time series, shape (T,).
Returns¶
FloatArray
The instantaneous phase of the series in [0, 2π).
Source code in src/scpn_phase_orchestrator/upde/market.py
market_order_parameter ¶
Return the Kuramoto order parameter R(t) across N assets.
Parameters¶
phases : FloatArray
Oscillator phases in radians, shape (N,).
Returns¶
FloatArray
The Kuramoto order parameter time series R(t).
Source code in src/scpn_phase_orchestrator/upde/market.py
market_plv ¶
Compute the rolling phase-locking-value matrix between assets.
Returns shape (T − window + 1, N, N).
Parameters¶
phases : FloatArray
Oscillator phases in radians, shape (N,).
window : int
Sliding-window length in samples.
Returns¶
FloatArray
The rolling phase-locking-value matrices, shape (T − window + 1, N, N).
Source code in src/scpn_phase_orchestrator/upde/market.py
detect_regimes ¶
detect_regimes(
R: FloatArray,
sync_threshold: float = 0.7,
desync_threshold: float = 0.3,
) -> IntArray
Classify market synchronisation regimes from R(t).
Returns int32 labels: 0 = desynchronised, 1 = transition,
2 = synchronised. O(T) masking; no multi-language port needed.
Parameters¶
R : FloatArray
Order-parameter time series R(t), shape (T,).
sync_threshold : float
Order parameter above which the market is classed as synchronised.
desync_threshold : float
Order parameter below which the market is classed as desynchronised.
Returns¶
IntArray The per-timestep market regime labels.
Raises¶
ValueError
If the thresholds are inconsistent or R is not 1-D.
Source code in src/scpn_phase_orchestrator/upde/market.py
sync_warning ¶
Detect synchronisation warnings where smoothed R crosses up.
Parameters¶
R : FloatArray
Order-parameter time series R(t), shape (T,).
threshold : float
Decision threshold.
lookback : int
Number of past samples smoothed over before the crossing test.
Returns¶
BoolArray A per-timestep boolean mask of synchronisation warnings.
Source code in src/scpn_phase_orchestrator/upde/market.py
6.2 Function Signatures¶
extract_phase(series: NDArray) → NDArray
| Parameter | Type | Description |
|---|---|---|
series |
(T,) or (T,N) |
Price/return time series |
| Returns | Same shape | Instantaneous phase \(\in [0, 2\pi)\) |
market_order_parameter(phases: NDArray) → NDArray
| Parameter | Type | Description |
|---|---|---|
phases |
(T, N) |
Phase matrix |
| Returns | (T,) |
\(R(t) \in [0, 1]\) |
market_plv(phases: NDArray, window: int) → NDArray
| Parameter | Type | Default | Description |
|---|---|---|---|
phases |
(T, N) |
— | Phase matrix |
window |
int |
50 |
Rolling window size |
| Returns | (T-W+1, N, N) |
PLV matrices |
detect_regimes(R: NDArray, sync_threshold, desync_threshold) → NDArray
| Parameter | Type | Default | Description |
|---|---|---|---|
R |
(T,) |
— | Order parameter time series |
sync_threshold |
float |
0.7 |
Sync boundary |
desync_threshold |
float |
0.3 |
Desync boundary |
| Returns | (T,) int |
0=desync, 1=transition, 2=sync |
sync_warning(R: NDArray, threshold, lookback) → NDArray
| Parameter | Type | Default | Description |
|---|---|---|---|
R |
(T,) |
— | Order parameter |
threshold |
float |
0.7 |
Warning level |
lookback |
int |
10 |
Smoothing window |
| Returns | (T,) bool |
True at crossing events |
7. Performance Benchmarks¶
7.1 Phase Extraction¶
Dominated by scipy.signal.hilbert (FFT-based):
| T | N | Time (ms) | Bottleneck |
|---|---|---|---|
| 500 | 10 | 0.5 | FFT |
| 5000 | 50 | 12 | FFT |
| 50000 | 100 | 350 | FFT + memory |
7.2 Market Order Parameter¶
| T | N | Python (µs) | Rust (µs) | Speedup |
|---|---|---|---|---|
| 500 | 10 | 120 | 25 | 4.8x |
| 5000 | 50 | 3200 | 480 | 6.7x |
| 50000 | 100 | 85000 | 9200 | 9.2x |
7.3 Market PLV (most expensive)¶
| T | N | W | Python (ms) | Rust (ms) | Speedup |
|---|---|---|---|---|---|
| 500 | 10 | 50 | 45 | 5.2 | 8.7x |
| 5000 | 50 | 50 | 4500 | 380 | 11.8x |
| 5000 | 50 | 200 | 4100 | 340 | 12.1x |
PLV is the computational bottleneck: \(O(T \times W \times N^2)\). Rust provides >10x speedup via Rayon parallelisation over time windows.
7.4 Complexity Summary¶
| Function | Time | Space |
|---|---|---|
extract_phase |
\(O(T N \log T)\) | \(O(T N)\) |
market_order_parameter |
\(O(T N)\) | \(O(T)\) |
market_plv |
\(O(T W N^2)\) | \(O(T N^2)\) |
detect_regimes |
\(O(T)\) | \(O(T)\) |
sync_warning |
\(O(T)\) | \(O(T)\) |
8. Citations¶
-
Münnix M.C., Shimada T., Schäfer R., et al. (2012). Identifying states of a financial market. Scientific Reports 2:644. doi:10.1038/srep00644
-
Harmon D., de Aguiar M.A.M., Chinellato D.D., Braha D., Epstein I.R., Bar-Yam Y. (2011). Predicting economic market crises using measures of collective panic. arXiv:1102.2620.
-
Kenett D.Y., Shapira Y., Madi A., Bransburg-Zabary S., Gur-Gershgoren G., Ben-Jacob E. (2012). Index Cohesive Force Analysis Reveals That the US Market Became Prone to Systemic Collapses Since 2002. PLoS ONE 7(2):e31144. doi:10.1371/journal.pone.0031144
-
Mantegna R.N., Stanley H.E. (1999). An Introduction to Econophysics: Correlations and Complexity in Finance. Cambridge University Press. ISBN 978-0-521-62008-7.
-
Pikovsky A., Rosenblum M., Kurths J. (2001). Synchronization: A Universal Concept in Nonlinear Sciences. Cambridge University Press. doi:10.1017/CBO9780511755743
-
Kritzman M., Li Y., Page S., Rigobon R. (2011). Principal Components as a Measure of Systemic Risk. Journal of Portfolio Management 37(4):112–126. doi:10.3905/jpm.2011.37.4.112
-
Marchenko V.A., Pastur L.A. (1967). Distribution of eigenvalues for some sets of random matrices. Mathematics of the USSR-Sbornik 1(4):457–483. doi:10.1070/SM1967v001n04ABEH001994
Test Coverage¶
tests/test_market.py— 18 tests: extract_phase shape, phase in [0,2π), R bounds [0,1], R=1 for identical phases, R≈0 for random, detect_regimes labels, sync_warning crossing detection, PLV shape, PLV symmetry, PLV range [0,1], window edge cases, Rust parity
Total: 18 tests.
Source¶
- Python:
src/scpn_phase_orchestrator/upde/market.py(161 lines) - Rust:
spo-kernel/crates/spo-engine/src/market.rs - FFI:
spo-kernel/crates/spo-ffi/src/lib.rs(market_order_parameter_rust, market_plv_rust, detect_regimes_rust)