Code / src/anomaly_atlas/stats/leadlag.py
src/anomaly_atlas/stats/leadlag.py
59 lines
# =============================================================================
# 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)