SPB Git

spb/anomaly-atlas Public License

Systematic discovery & rigorous validation of statistical anomalies in open HF market data (hfmarketdata.io) — pre-registered, artifact-null-driven, fully reproducible. Live atlas: www.anomaly-atlas.io

Python 61.4% JavaScript 28.7% CSS 8.6% Shell 0.7% Makefile 0.5%
2.8 KB · 84 lines python
Raw Blame History
1# =============================================================================2#  Project   : anomaly-atlas3#  File      : src/anomaly_atlas/stats/reversion.py4#  Purpose   : Mean-reversion tests: variance ratios, AC1, AR half-life5#  Author    : Simon-Pierre Boucher6#  Contact   : contact@spboucher.ai7#  Data src  : hfmarketdata.io (sole data source)8#  Created   : 2026-08-129#  Modified  : 2026-08-1210#  Platform  : macOS / Apple Silicon (arm64)11#  License   : All rights reserved (research code)12# =============================================================================13"""Mean-reversion statistics on return series.1415Validated on synthetic ground truth before touching real data16(benchmarks/synthetic/test_synthetic_gate.py — charter §8.1).17"""1819from __future__ import annotations2021import numpy as np222324def ac1(returns: np.ndarray) -> float:25    """Lag-1 autocorrelation of a return series."""26    r = np.asarray(returns, dtype=float)27    if len(r) < 3:28        return float("nan")29    a, b = r[:-1], r[1:]30    sa, sb = a.std(), b.std()31    if sa == 0.0 or sb == 0.0:32        return float("nan")33    return float(((a - a.mean()) * (b - b.mean())).mean() / (sa * sb))343536def autocov1(returns: np.ndarray) -> float:37    """Lag-1 autocovariance (input to the Roll spread estimator)."""38    r = np.asarray(returns, dtype=float)39    if len(r) < 3:40        return float("nan")41    a, b = r[:-1], r[1:]42    return float(((a - a.mean()) * (b - b.mean())).mean())434445def variance_ratio(returns: np.ndarray, q: int) -> float:46    """Lo-MacKinlay variance ratio VR(q) with overlapping q-period sums.4748    Ground truth: VR = 1 for a random walk, < 1 under mean reversion,49    > 1 under momentum. Unbiased variance estimators, demeaned.50    """51    r = np.asarray(returns, dtype=float)52    n = len(r)53    if n < q + 2 or q < 2:54        return float("nan")55    mu = r.mean()56    var1 = ((r - mu) ** 2).sum() / (n - 1)57    rq = np.convolve(r, np.ones(q), mode="valid")  # overlapping q-sums58    # Lo-MacKinlay bias-corrected PER-PERIOD variance of q-sums: the factor q59    # lives inside m, so the ratio below is varq/var1 (NOT varq/(q*var1)).60    m = q * (n - q + 1) * (1 - q / n)61    varq = ((rq - q * mu) ** 2).sum() / m62    if var1 == 0.0:63        return float("nan")64    return float(varq / var1)656667def half_life(log_prices: np.ndarray) -> float:68    """Mean-reversion half-life from an AR(1) fit: dp_t = a + b*p_{t-1} + e.6970    Returns ln(2)/-ln(1+b) in bars for b in (-1, 0); +inf if b >= 071    (no reversion). Matches the OU generator's ln(2)/-ln(1-kappa).72    """73    p = np.asarray(log_prices, dtype=float)74    if len(p) < 10:75        return float("nan")76    x, y = p[:-1], np.diff(p)77    vx = x.var()78    if vx == 0.0:79        return float("nan")80    b = ((x - x.mean()) * (y - y.mean())).mean() / vx81    if b >= 0.0 or b <= -1.0:82        return float("inf")83    return float(np.log(2.0) / -np.log1p(b))84