Code / benchmarks/synthetic/test_gate_expf.py
benchmarks/synthetic/test_gate_expf.py
93 lines
# =============================================================================
# Project : anomaly-atlas
# File : benchmarks/synthetic/test_gate_expf.py
# Purpose : §8.1 gate for expF: Reality Check, SPA, Deflated Sharpe
# 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 correction battery before it touches real scan output:
* a universe of PURE-NOISE rules must not survive RC/SPA (p not small),
and its best Sharpe must be fully explained by selection (DSR ~ 0);
* a planted genuinely profitable rule must survive all three;
* the bootstrap must respect serial dependence (block structure).
"""
from __future__ import annotations
import numpy as np
from anomaly_atlas.stats.spa import deflated_sharpe, reality_check, spa_test
T, N = 1500, 60 # ~6 years of days, 60 searched rules
def noise_matrix(seed: int) -> np.ndarray:
rng = np.random.default_rng(seed)
return rng.normal(0.0, 0.01, (T, N))
def test_pure_noise_universe_does_not_survive():
ps_rc, ps_spa = [], []
for seed in (1, 2, 3, 4, 5):
x = noise_matrix(seed)
ps_rc.append(reality_check(x, n_boot=300, seed=seed)["p"])
ps_spa.append(spa_test(x, n_boot=300, seed=seed)["p"])
# under H0 the p-values should look uniform: none should be tiny,
# and on average they must be comfortably away from 0
assert min(ps_rc) > 0.01 and np.mean(ps_rc) > 0.2
assert min(ps_spa) > 0.01 and np.mean(ps_spa) > 0.2
def test_planted_profitable_rule_survives_rc_and_spa():
for seed in (1, 2, 3):
x = noise_matrix(seed)
rng = np.random.default_rng(seed + 100)
x[:, 7] = rng.normal(0.0015, 0.01, T) # daily SR ~ 0.15 (t ~ 5.8)
rc = reality_check(x, n_boot=300, seed=seed)
sp = spa_test(x, n_boot=300, seed=seed)
assert rc["p"] < 0.02 and rc["best_rule"] == 7
assert sp["p"] < 0.02 and sp["best_rule"] == 7
def test_dsr_kills_noise_best_and_keeps_planted():
rng = np.random.default_rng(9)
x = noise_matrix(9)
srs = x.mean(axis=0) / x.std(axis=0, ddof=1)
best = int(np.argmax(srs))
r = x[:, best]
d_noise = deflated_sharpe(
sr=float(srs[best]), t_len=T,
skew=float(((r - r.mean()) ** 3).mean() / r.std() ** 3),
kurt=float(((r - r.mean()) ** 4).mean() / r.std() ** 4),
n_trials=N, sr_variance=float(srs.var(ddof=1)),
)
assert d_noise["dsr"] < 0.6 # selection explains the best noise Sharpe
# planted strong rule
x[:, 3] = rng.normal(0.002, 0.01, T)
srs2 = x.mean(axis=0) / x.std(axis=0, ddof=1)
r2 = x[:, 3]
d_real = deflated_sharpe(
sr=float(srs2[3]), t_len=T,
skew=float(((r2 - r2.mean()) ** 3).mean() / r2.std() ** 3),
kurt=float(((r2 - r2.mean()) ** 4).mean() / r2.std() ** 4),
n_trials=N, sr_variance=float(srs2.var(ddof=1)),
)
assert d_real["dsr"] > 0.95
assert d_noise["dsr"] < d_real["dsr"]
def test_stationary_bootstrap_preserves_dependence():
from anomaly_atlas.stats.spa import stationary_bootstrap_indices
idx = stationary_bootstrap_indices(1000, mean_block=20, n_boot=50, seed=3)
# consecutive indices should usually be consecutive (inside a block)
consecutive = np.mean((np.diff(idx, axis=1) % 1000) == 1)
assert consecutive > 0.9 # mean block 20 -> ~95% of steps are within-block
assert idx.min() >= 0 and idx.max() < 1000