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%
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