# ============================================================================= # Project : anomaly-atlas # File : benchmarks/synthetic/test_gate_expc.py # Purpose : ยง8.1 gate for the expC additions: EDGE spread, FDR, bootstrap p # 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) # ============================================================================= """Gate the detectors added for expC before they touch real data: * EDGE spread from synthetic OHLC bars: recovers a planted Roll spread, reads ~0 on a spread-free random walk; * Benjamini-Hochberg: controls FDR on uniform nulls, finds planted signal; * bootstrap_pvalue: uniform-ish under the null, small under a real effect. """ from __future__ import annotations import sys from pathlib import Path import numpy as np sys.path.insert(0, str(Path(__file__).resolve().parent)) from generators import random_walk, roll_bounce_prices # noqa: E402 from anomaly_atlas.stats.bootstrap import moving_block_bootstrap from anomaly_atlas.stats.multiple_testing import ( benjamini_hochberg, bonferroni, bootstrap_pvalue, ) from anomaly_atlas.stats.reversion import ac1 from anomaly_atlas.validation.artifacts import edge_spread SEEDS = [1, 2, 3, 4, 5] def bars_from_ticks(log_prices: np.ndarray, per_bar: int = 30): """Aggregate a synthetic tick path into OHLC bars (price space).""" n = (len(log_prices) // per_bar) * per_bar p = np.exp(log_prices[:n]).reshape(-1, per_bar) return p[:, 0], p.max(axis=1), p.min(axis=1), p[:, -1] # ----------------------------------------------------------------- EDGE gate def test_edge_recovers_planted_spread_from_bars(): spread = 0.004 for seed in SEEDS: ticks = roll_bounce_prices(120_000, spread=spread, sigma=0.0008, seed=seed) o, h, low, c = bars_from_ticks(ticks) est = edge_spread(o, h, low, c) assert np.isfinite(est) assert abs(est - spread) / spread < 0.30 # bar aggregation loses info def test_edge_reads_near_zero_on_spreadless_walk(): for seed in SEEDS: ticks = random_walk(120_000, sigma=0.0008, seed=seed) o, h, low, c = bars_from_ticks(ticks) est = edge_spread(o, h, low, c) # undefined (NaN) or tiny relative to the planted case assert (not np.isfinite(est)) or est < 0.001 # ------------------------------------------------------------------ FDR gate def test_bh_controls_false_discoveries_on_pure_null(): rng = np.random.default_rng(7) false_rates = [] for _ in range(200): p = rng.random(100) # all null false_rates.append(benjamini_hochberg(p, alpha=0.05).mean()) assert np.mean(false_rates) < 0.05 # FDR controlled def test_bh_finds_planted_signal_and_bonferroni_is_stricter(): rng = np.random.default_rng(8) p = np.concatenate([rng.random(90), rng.random(10) * 1e-5]) # 10 real bh = benjamini_hochberg(p, alpha=0.05) bf = bonferroni(p, alpha=0.05) assert bh[90:].all() # all planted found assert bh.sum() >= bf.sum() # BH never stricter than Bonferroni assert bh[:90].sum() <= 5 # few false positives def test_bh_counts_nan_toward_m(): p = np.array([0.001, np.nan, np.nan, np.nan]) # m=4: threshold for rank 1 is 0.05/4=0.0125 -> still rejected assert benjamini_hochberg(p, alpha=0.05)[0] assert not benjamini_hochberg(p, alpha=0.05)[1:].any() # ------------------------------------------------------- bootstrap p-value gate def test_bootstrap_pvalue_calibration(): # null: AC1 of a random walk -> p should be comfortably non-small r = np.diff(random_walk(60_000, seed=3)) boot = moving_block_bootstrap(r, ac1, block=390, n_boot=300, seed=3) assert bootstrap_pvalue(boot, 0.0) > 0.05 # real effect: AC1 of a bounce series -> tiny p rb = np.diff(roll_bounce_prices(60_000, spread=0.003, seed=3)) boot_b = moving_block_bootstrap(rb, ac1, block=390, n_boot=300, seed=3) assert bootstrap_pvalue(boot_b, 0.0) < 0.01