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.1 KB · 108 lines python
Raw Blame History
1# =============================================================================2#  Project   : anomaly-atlas3#  File      : benchmarks/synthetic/test_gate_expc.py4#  Purpose   : §8.1 gate for the expC additions: EDGE spread, FDR, bootstrap p5#  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"""Gate the detectors added for expC before they touch real data:1415  * EDGE spread from synthetic OHLC bars: recovers a planted Roll spread,16    reads ~0 on a spread-free random walk;17  * Benjamini-Hochberg: controls FDR on uniform nulls, finds planted signal;18  * bootstrap_pvalue: uniform-ish under the null, small under a real effect.19"""2021from __future__ import annotations2223import sys24from pathlib import Path2526import numpy as np2728sys.path.insert(0, str(Path(__file__).resolve().parent))2930from generators import random_walk, roll_bounce_prices  # noqa: E4023132from anomaly_atlas.stats.bootstrap import moving_block_bootstrap33from anomaly_atlas.stats.multiple_testing import (34    benjamini_hochberg,35    bonferroni,36    bootstrap_pvalue,37)38from anomaly_atlas.stats.reversion import ac139from anomaly_atlas.validation.artifacts import edge_spread4041SEEDS = [1, 2, 3, 4, 5]424344def bars_from_ticks(log_prices: np.ndarray, per_bar: int = 30):45    """Aggregate a synthetic tick path into OHLC bars (price space)."""46    n = (len(log_prices) // per_bar) * per_bar47    p = np.exp(log_prices[:n]).reshape(-1, per_bar)48    return p[:, 0], p.max(axis=1), p.min(axis=1), p[:, -1]495051# ----------------------------------------------------------------- EDGE gate52def test_edge_recovers_planted_spread_from_bars():53    spread = 0.00454    for seed in SEEDS:55        ticks = roll_bounce_prices(120_000, spread=spread, sigma=0.0008, seed=seed)56        o, h, low, c = bars_from_ticks(ticks)57        est = edge_spread(o, h, low, c)58        assert np.isfinite(est)59        assert abs(est - spread) / spread < 0.30  # bar aggregation loses info606162def test_edge_reads_near_zero_on_spreadless_walk():63    for seed in SEEDS:64        ticks = random_walk(120_000, sigma=0.0008, seed=seed)65        o, h, low, c = bars_from_ticks(ticks)66        est = edge_spread(o, h, low, c)67        # undefined (NaN) or tiny relative to the planted case68        assert (not np.isfinite(est)) or est < 0.001697071# ------------------------------------------------------------------ FDR gate72def test_bh_controls_false_discoveries_on_pure_null():73    rng = np.random.default_rng(7)74    false_rates = []75    for _ in range(200):76        p = rng.random(100)  # all null77        false_rates.append(benjamini_hochberg(p, alpha=0.05).mean())78    assert np.mean(false_rates) < 0.05  # FDR controlled798081def test_bh_finds_planted_signal_and_bonferroni_is_stricter():82    rng = np.random.default_rng(8)83    p = np.concatenate([rng.random(90), rng.random(10) * 1e-5])  # 10 real84    bh = benjamini_hochberg(p, alpha=0.05)85    bf = bonferroni(p, alpha=0.05)86    assert bh[90:].all()  # all planted found87    assert bh.sum() >= bf.sum()  # BH never stricter than Bonferroni88    assert bh[:90].sum() <= 5  # few false positives899091def test_bh_counts_nan_toward_m():92    p = np.array([0.001, np.nan, np.nan, np.nan])93    # m=4: threshold for rank 1 is 0.05/4=0.0125 -> still rejected94    assert benjamini_hochberg(p, alpha=0.05)[0]95    assert not benjamini_hochberg(p, alpha=0.05)[1:].any()969798# ------------------------------------------------------- bootstrap p-value gate99def test_bootstrap_pvalue_calibration():100    # null: AC1 of a random walk -> p should be comfortably non-small101    r = np.diff(random_walk(60_000, seed=3))102    boot = moving_block_bootstrap(r, ac1, block=390, n_boot=300, seed=3)103    assert bootstrap_pvalue(boot, 0.0) > 0.05104    # real effect: AC1 of a bounce series -> tiny p105    rb = np.diff(roll_bounce_prices(60_000, spread=0.003, seed=3))106    boot_b = moving_block_bootstrap(rb, ac1, block=390, n_boot=300, seed=3)107    assert bootstrap_pvalue(boot_b, 0.0) < 0.01108