# ============================================================================= # Project : anomaly-atlas # File : src/anomaly_atlas/stats/leadlag.py # Purpose : Lead-lag tests: lagged cross-correlation and asymmetry # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Data src : hfmarketdata.io (sole data source) # Created : 2026-08-12 # Modified : 2026-08-12 # Platform : macOS / Apple Silicon (arm64) # License : All rights reserved (research code) # ============================================================================= """Lagged cross-correlation between two return series. Sign convention: lag k > 0 means x LEADS y by k bars — corr(x_{t-k}, y_t). Validated on synthetic ground truth (charter §8.1) before real data. """ from __future__ import annotations import numpy as np def lagged_xcorr(x: np.ndarray, y: np.ndarray, max_lag: int) -> dict[int, float]: """corr(x_{t-k}, y_t) for k in [-max_lag, +max_lag]; k>0 = x leads y.""" x = np.asarray(x, dtype=float) y = np.asarray(y, dtype=float) n = min(len(x), len(y)) x, y = x[:n], y[:n] out: dict[int, float] = {} for k in range(-max_lag, max_lag + 1): if k >= 0: a, b = x[: n - k] if k else x, y[k:] if k else y else: a, b = x[-k:], y[: n + k] if len(a) < 3 or a.std() == 0.0 or b.std() == 0.0: out[k] = float("nan") continue out[k] = float(np.corrcoef(a, b)[0, 1]) return out def peak_lag(xc: dict[int, float]) -> int: """Lag with the largest |corr| (ties: smallest |lag|).""" finite = {k: v for k, v in xc.items() if np.isfinite(v)} if not finite: return 0 return min(finite, key=lambda k: (-abs(finite[k]), abs(k))) def leadlag_asymmetry(xc: dict[int, float]) -> float: """Sum of corr at positive lags minus sum at negative lags. Zero (in expectation) for synchronous series; positive when x leads y. """ pos = sum(v for k, v in xc.items() if k > 0 and np.isfinite(v)) neg = sum(v for k, v in xc.items() if k < 0 and np.isfinite(v)) return float(pos - neg)