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 : benchmarks/synthetic/test_synthetic_gate.py4# Purpose : §8.1 gate — detectors must pass synthetic ground truth first5# 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"""The mandatory gate of charter §8.1, as executable tests:1415 1. pure random walk -> NO anomaly may be detected16 2. planted mean-reversion -> must be recovered (incl. half-life)17 3. planted lead-lag -> must be recovered at the right lag18 4. planted calendar effect -> must be recovered on the right phase19 5. pure bid-ask bounce -> must be flagged as ARTIFACT, not anomaly20 6. staleness (LOCF) -> must manufacture the documented artifacts2122A detector that fails any of these is broken and must not touch real data.23Multi-seed checks use fixed seed lists — fully deterministic.24"""2526from __future__ import annotations2728import sys29from pathlib import Path3031import numpy as np3233sys.path.insert(0, str(Path(__file__).resolve().parent))3435from generators import ( # noqa: E40236 correlated_pair,37 leadlag_pair,38 ou_prices,39 random_walk,40 roll_bounce_prices,41 seasonal_returns,42 stale_observe,43)4445from anomaly_atlas.stats.bootstrap import moving_block_bootstrap, percentile_ci46from anomaly_atlas.stats.leadlag import lagged_xcorr, leadlag_asymmetry, peak_lag47from anomaly_atlas.stats.reversion import ac1, half_life, variance_ratio48from anomaly_atlas.validation.artifacts import (49 excess_reversion,50 locf_fill,51 roll_spread,52 staleness_ratio,53)5455N = 100_00056SEEDS = [1, 2, 3, 4, 5]575859# ----------------------------------------------------- 1. random walk: nothing60def test_random_walk_triggers_nothing():61 for seed in SEEDS:62 r = np.diff(random_walk(N, seed=seed))63 assert abs(ac1(r)) < 0.0264 assert abs(variance_ratio(r, 5) - 1.0) < 0.0565 assert abs(variance_ratio(r, 30) - 1.0) < 0.1266 assert half_life(random_walk(N, seed=seed)) > 5_000 # effectively none676869def test_random_walk_ac1_inside_its_bootstrap_ci():70 r = np.diff(random_walk(N, seed=7))71 boot = moving_block_bootstrap(r, ac1, block=390, n_boot=200, seed=7)72 lo, hi = percentile_ci(boot)73 assert lo < 0.0 < hi # zero is inside the CI: no detection747576def test_independent_walks_show_no_leadlag():77 for seed in SEEDS:78 x = np.diff(random_walk(N, seed=seed))79 y = np.diff(random_walk(N, seed=seed + 100))80 xc = lagged_xcorr(x, y, 5)81 assert max(abs(v) for v in xc.values()) < 0.0282 assert abs(leadlag_asymmetry(xc)) < 0.05838485# ------------------------------------------- 2. planted reversion is recovered86def test_ou_reversion_recovered_with_half_life():87 kappa = 0.02 # true half-life = ln2 / -ln(0.98) ≈ 34.3 bars88 true_hl = np.log(2) / -np.log(1 - kappa)89 for seed in SEEDS:90 p = ou_prices(N, kappa=kappa, seed=seed)91 r = np.diff(p)92 assert ac1(r) < -0.00593 assert variance_ratio(r, 30) < 0.994 assert abs(half_life(p) - true_hl) / true_hl < 0.25959697# --------------------------------------------- 3. planted lead-lag is recovered98def test_planted_leadlag_recovered_at_correct_lag():99 for seed in SEEDS:100 x, y = leadlag_pair(N, beta=0.3, lag=2, seed=seed)101 xc = lagged_xcorr(x, y, 5)102 assert peak_lag(xc) == 2103 assert xc[2] > 0.2104 assert abs(xc[1]) < 0.02 and abs(xc[3]) < 0.02105106107# --------------------------------------- 4. planted calendar effect is recovered108def test_planted_seasonal_effect_recovered_on_right_phase():109 period, hot, amp = 5, 3, 0.0005110 for seed in SEEDS:111 r = seasonal_returns(N, period, hot, amp, seed=seed)112 phase_means = [r[np.arange(N) % period == k].mean() for k in range(period)]113 assert np.argmax(phase_means) == hot114 assert abs(phase_means[hot] - amp) < amp * 0.2115 rest = [m for k, m in enumerate(phase_means) if k != hot]116 assert max(abs(m) for m in rest) < amp * 0.2117118119# ------------------------------------- 5. pure bounce is an artifact, not a find120def test_roll_spread_recovers_planted_spread():121 spread = 0.002122 for seed in SEEDS:123 p = roll_bounce_prices(N, spread=spread, seed=seed)124 est = roll_spread(np.diff(p))125 assert abs(est - spread) / spread < 0.10126127128def test_pure_bounce_reversion_vanishes_after_artifact_adjustment():129 spread = 0.002130 for seed in SEEDS:131 p = roll_bounce_prices(N, spread=spread, seed=seed)132 r = np.diff(p)133 assert ac1(r) < -0.2 # naive detector screams "mean reversion!"134 # ...but the excess over the bounce null (true spread supplied) is ~0135 assert abs(excess_reversion(r, spread)) < 0.03136137138def test_true_reversion_survives_artifact_adjustment():139 # OU + bounce: after removing the bounce share, reversion must REMAIN140 kappa, spread = 0.05, 0.001141 for seed in SEEDS:142 mid = ou_prices(N, kappa=kappa, seed=seed)143 rng = np.random.default_rng(seed + 999)144 p = mid + (spread / 2.0) * rng.choice([-1.0, 1.0], size=N)145 r = np.diff(p)146 assert excess_reversion(r, spread) < -0.01147148149# ------------------------------------------------ 6. staleness manufactures lies150def test_locf_creates_spurious_positive_autocorrelation():151 for seed in SEEDS:152 p = random_walk(N, seed=seed)153 locf, mask = stale_observe(p, p_observe=0.3, seed=seed)154 r = np.diff(locf)155 assert ac1(np.diff(p)) < 0.02 # underlying: nothing156 # LOCF returns of a pure walk: AC1 pushed NEGATIVE at lag 1 grid steps157 # is not the failure mode; the artifact is CROSS-serial (next test) and158 # a big mass of zero returns. Document the zero-mass here:159 assert (r == 0).mean() > 0.5160 assert staleness_ratio(mask) > 0.6161162163def test_locf_makes_fresh_series_appear_to_lead_stale_one():164 for seed in SEEDS:165 px, py = correlated_pair(N, rho=0.7, seed=seed)166 # underlying returns: correlation only at lag 0167 xc_true = lagged_xcorr(np.diff(px), np.diff(py), 3)168 assert abs(xc_true[1]) < 0.02169 # y observed sparsely, LOCF-joined on the grid: x now "leads" y170 mask = np.random.default_rng(seed).random(N) < 0.3171 mask[0] = True172 y_locf = locf_fill(py, mask)173 xc = lagged_xcorr(np.diff(px), np.diff(y_locf), 3)174 assert xc[1] > 0.10 # spurious lead of the fresh series175 assert leadlag_asymmetry(xc) > 0.1176 assert peak_lag({k: v for k, v in xc.items() if k != 0}) == 1177