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%
3.6 KB · 93 lines python
Raw Blame History
1# =============================================================================2#  Project   : anomaly-atlas3#  File      : benchmarks/synthetic/test_gate_expf.py4#  Purpose   : §8.1 gate for expF: Reality Check, SPA, Deflated Sharpe5#  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 correction battery before it touches real scan output:1415  * a universe of PURE-NOISE rules must not survive RC/SPA (p not small),16    and its best Sharpe must be fully explained by selection (DSR ~ 0);17  * a planted genuinely profitable rule must survive all three;18  * the bootstrap must respect serial dependence (block structure).19"""2021from __future__ import annotations2223import numpy as np2425from anomaly_atlas.stats.spa import deflated_sharpe, reality_check, spa_test2627T, N = 1500, 60  # ~6 years of days, 60 searched rules282930def noise_matrix(seed: int) -> np.ndarray:31    rng = np.random.default_rng(seed)32    return rng.normal(0.0, 0.01, (T, N))333435def test_pure_noise_universe_does_not_survive():36    ps_rc, ps_spa = [], []37    for seed in (1, 2, 3, 4, 5):38        x = noise_matrix(seed)39        ps_rc.append(reality_check(x, n_boot=300, seed=seed)["p"])40        ps_spa.append(spa_test(x, n_boot=300, seed=seed)["p"])41    # under H0 the p-values should look uniform: none should be tiny,42    # and on average they must be comfortably away from 043    assert min(ps_rc) > 0.01 and np.mean(ps_rc) > 0.244    assert min(ps_spa) > 0.01 and np.mean(ps_spa) > 0.2454647def test_planted_profitable_rule_survives_rc_and_spa():48    for seed in (1, 2, 3):49        x = noise_matrix(seed)50        rng = np.random.default_rng(seed + 100)51        x[:, 7] = rng.normal(0.0015, 0.01, T)  # daily SR ~ 0.15 (t ~ 5.8)52        rc = reality_check(x, n_boot=300, seed=seed)53        sp = spa_test(x, n_boot=300, seed=seed)54        assert rc["p"] < 0.02 and rc["best_rule"] == 755        assert sp["p"] < 0.02 and sp["best_rule"] == 7565758def test_dsr_kills_noise_best_and_keeps_planted():59    rng = np.random.default_rng(9)60    x = noise_matrix(9)61    srs = x.mean(axis=0) / x.std(axis=0, ddof=1)62    best = int(np.argmax(srs))63    r = x[:, best]64    d_noise = deflated_sharpe(65        sr=float(srs[best]), t_len=T,66        skew=float(((r - r.mean()) ** 3).mean() / r.std() ** 3),67        kurt=float(((r - r.mean()) ** 4).mean() / r.std() ** 4),68        n_trials=N, sr_variance=float(srs.var(ddof=1)),69    )70    assert d_noise["dsr"] < 0.6  # selection explains the best noise Sharpe71    # planted strong rule72    x[:, 3] = rng.normal(0.002, 0.01, T)73    srs2 = x.mean(axis=0) / x.std(axis=0, ddof=1)74    r2 = x[:, 3]75    d_real = deflated_sharpe(76        sr=float(srs2[3]), t_len=T,77        skew=float(((r2 - r2.mean()) ** 3).mean() / r2.std() ** 3),78        kurt=float(((r2 - r2.mean()) ** 4).mean() / r2.std() ** 4),79        n_trials=N, sr_variance=float(srs2.var(ddof=1)),80    )81    assert d_real["dsr"] > 0.9582    assert d_noise["dsr"] < d_real["dsr"]838485def test_stationary_bootstrap_preserves_dependence():86    from anomaly_atlas.stats.spa import stationary_bootstrap_indices8788    idx = stationary_bootstrap_indices(1000, mean_block=20, n_boot=50, seed=3)89    # consecutive indices should usually be consecutive (inside a block)90    consecutive = np.mean((np.diff(idx, axis=1) % 1000) == 1)91    assert consecutive > 0.9  # mean block 20 -> ~95% of steps are within-block92    assert idx.min() >= 0 and idx.max() < 100093