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%
4.3 KB · 120 lines python
Raw Blame History
1# =============================================================================2#  Project   : anomaly-atlas3#  File      : src/anomaly_atlas/validation/artifacts.py4#  Purpose   : Artifact detectors: Roll bounce, staleness, LOCF resampling5#  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"""Detectors for the mechanisms that manufacture fake anomalies in bar data.1415Doctrine (charter §2.1): every candidate anomaly must first be explained by16these nulls before it may be called a regularity. Each function is validated17on synthetic ground truth (charter §8.1).18"""1920from __future__ import annotations2122import numpy as np2324from anomaly_atlas.stats.reversion import autocov1252627def roll_spread(returns: np.ndarray) -> float:28    """Roll (1984) implied effective spread: 2*sqrt(-Cov(r_t, r_{t-1})).2930    In log-return space this is the RELATIVE spread. Returns NaN when the31    lag-1 autocovariance is non-negative (estimator undefined — typical for32    momentum or noise-free series).33    """34    cov = autocov1(returns)35    if not np.isfinite(cov) or cov >= 0.0:36        return float("nan")37    return float(2.0 * np.sqrt(-cov))383940def bounce_implied_ac1(returns: np.ndarray) -> float:41    """The lag-1 autocorrelation a pure Roll bounce would produce for this42    series: -s^2/4 divided by Var(r), with s the Roll implied spread.4344    Because s is estimated FROM the lag-1 autocovariance, this equals the45    measured AC1 whenever AC1 < 0 — the useful output is the DECOMPOSITION:46    ``excess_reversion`` reports how much reversion remains after removing47    the bounce explainable by the observed spread level.48    """49    r = np.asarray(returns, dtype=float)50    s = roll_spread(r)51    if not np.isfinite(s):52        return 0.053    var = r.var()54    if var == 0.0:55        return float("nan")56    return float(-(s**2) / 4.0 / var)575859def excess_reversion(returns: np.ndarray, rel_spread: float) -> float:60    """Artifact-adjusted AC1: measured AC1 minus the bounce null implied by an61    INDEPENDENT spread estimate ``rel_spread`` (e.g. a liquidity-matched62    spread level, or a quoted/estimated spread from another source).6364    For a pure Roll series with the true spread supplied, this is ≈ 0.65    A genuinely mean-reverting series keeps a negative excess.66    """67    r = np.asarray(returns, dtype=float)68    var = r.var()69    if var == 0.0 or len(r) < 3:70        return float("nan")71    from anomaly_atlas.stats.reversion import ac17273    bounce_ac1 = -(rel_spread**2) / 4.0 / var74    return float(ac1(r) - bounce_ac1)757677def edge_spread(78    opens: np.ndarray, highs: np.ndarray, lows: np.ndarray, closes: np.ndarray79) -> float:80    """EDGE relative effective spread (Ardia, Guidotti & Kroencke 2024) from81    OHLC bars, via the authors' `bidask` implementation.8283    Independent of 1min AC1 (uses O/H/L/C geometry), so it can serve as the84    independent spread input to `excess_reversion` without circularity.85    Returns NaN when the estimator is undefined for the sample.86    """87    from bidask import edge8889    try:90        est = edge(91            np.asarray(opens, float), np.asarray(highs, float),92            np.asarray(lows, float), np.asarray(closes, float),93        )94    except Exception:95        return float("nan")96    return float(est) if np.isfinite(est) else float("nan")979899def staleness_ratio(observed_mask: np.ndarray) -> float:100    """Fraction of grid slots WITHOUT a fresh print (0 = fully fresh)."""101    m = np.asarray(observed_mask, dtype=bool)102    if len(m) == 0:103        return float("nan")104    return float(1.0 - m.mean())105106107def locf_fill(values: np.ndarray, observed_mask: np.ndarray) -> np.ndarray:108    """Last-observation-carried-forward fill of a gridded series.109110    Slots before the first observation keep their original value. This is111    the (dangerous) join that manufactures stale-price artifacts — it exists112    here so experiments can measure that artifact explicitly.113    """114    v = np.asarray(values, dtype=float).copy()115    m = np.asarray(observed_mask, dtype=bool)116    for t in range(1, len(v)):117        if not m[t]:118            v[t] = v[t - 1]119    return v120