Honesty doctrine. Every candidate anomaly is an artifact until proven otherwise; in-sample results are never findings; past statistical regularity does not imply future returns. This is research on statistical properties of market data — not investment advice, not a trading system.

Code / src/anomaly_atlas/validation/artifacts.py

src/anomaly_atlas/validation/artifacts.py 120 lines
# =============================================================================
#  Project   : anomaly-atlas
#  File      : src/anomaly_atlas/validation/artifacts.py
#  Purpose   : Artifact detectors: Roll bounce, staleness, LOCF resampling
#  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)
# =============================================================================
"""Detectors for the mechanisms that manufacture fake anomalies in bar data.

Doctrine (charter §2.1): every candidate anomaly must first be explained by
these nulls before it may be called a regularity. Each function is validated
on synthetic ground truth (charter §8.1).
"""

from __future__ import annotations

import numpy as np

from anomaly_atlas.stats.reversion import autocov1


def roll_spread(returns: np.ndarray) -> float:
    """Roll (1984) implied effective spread: 2*sqrt(-Cov(r_t, r_{t-1})).

    In log-return space this is the RELATIVE spread. Returns NaN when the
    lag-1 autocovariance is non-negative (estimator undefined — typical for
    momentum or noise-free series).
    """
    cov = autocov1(returns)
    if not np.isfinite(cov) or cov >= 0.0:
        return float("nan")
    return float(2.0 * np.sqrt(-cov))


def bounce_implied_ac1(returns: np.ndarray) -> float:
    """The lag-1 autocorrelation a pure Roll bounce would produce for this
    series: -s^2/4 divided by Var(r), with s the Roll implied spread.

    Because s is estimated FROM the lag-1 autocovariance, this equals the
    measured AC1 whenever AC1 < 0 — the useful output is the DECOMPOSITION:
    ``excess_reversion`` reports how much reversion remains after removing
    the bounce explainable by the observed spread level.
    """
    r = np.asarray(returns, dtype=float)
    s = roll_spread(r)
    if not np.isfinite(s):
        return 0.0
    var = r.var()
    if var == 0.0:
        return float("nan")
    return float(-(s**2) / 4.0 / var)


def excess_reversion(returns: np.ndarray, rel_spread: float) -> float:
    """Artifact-adjusted AC1: measured AC1 minus the bounce null implied by an
    INDEPENDENT spread estimate ``rel_spread`` (e.g. a liquidity-matched
    spread level, or a quoted/estimated spread from another source).

    For a pure Roll series with the true spread supplied, this is ≈ 0.
    A genuinely mean-reverting series keeps a negative excess.
    """
    r = np.asarray(returns, dtype=float)
    var = r.var()
    if var == 0.0 or len(r) < 3:
        return float("nan")
    from anomaly_atlas.stats.reversion import ac1

    bounce_ac1 = -(rel_spread**2) / 4.0 / var
    return float(ac1(r) - bounce_ac1)


def edge_spread(
    opens: np.ndarray, highs: np.ndarray, lows: np.ndarray, closes: np.ndarray
) -> float:
    """EDGE relative effective spread (Ardia, Guidotti & Kroencke 2024) from
    OHLC bars, via the authors' `bidask` implementation.

    Independent of 1min AC1 (uses O/H/L/C geometry), so it can serve as the
    independent spread input to `excess_reversion` without circularity.
    Returns NaN when the estimator is undefined for the sample.
    """
    from bidask import edge

    try:
        est = edge(
            np.asarray(opens, float), np.asarray(highs, float),
            np.asarray(lows, float), np.asarray(closes, float),
        )
    except Exception:
        return float("nan")
    return float(est) if np.isfinite(est) else float("nan")


def staleness_ratio(observed_mask: np.ndarray) -> float:
    """Fraction of grid slots WITHOUT a fresh print (0 = fully fresh)."""
    m = np.asarray(observed_mask, dtype=bool)
    if len(m) == 0:
        return float("nan")
    return float(1.0 - m.mean())


def locf_fill(values: np.ndarray, observed_mask: np.ndarray) -> np.ndarray:
    """Last-observation-carried-forward fill of a gridded series.

    Slots before the first observation keep their original value. This is
    the (dangerous) join that manufactures stale-price artifacts — it exists
    here so experiments can measure that artifact explicitly.
    """
    v = np.asarray(values, dtype=float).copy()
    m = np.asarray(observed_mask, dtype=bool)
    for t in range(1, len(v)):
        if not m[t]:
            v[t] = v[t - 1]
    return v