expB: artifact baselines measured (after §8.1 synthetic gate)
- synthetic generators with known ground truth + 11-test gate; the gate caught a real VR estimator bug (double division by q) before real data - stats: reversion (VR/AC1/half-life), leadlag, block bootstrap; validation: Roll spread, excess reversion, staleness, LOCF - expB (pre-specified protocol): 31 tickers Q1 2024 RTH 1min — bounce AC1 -0.009/-0.051/-0.232 by staleness tercile, VR30 down to 0.55 with zero economics, SPY spuriously leads stale names (+0.047 at +1min, Spearman +0.43), SPX lags SPY by 1min (+0.065) - artifact_taxonomy.md: 7 entries T1-T7 with measured magnitudes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Showing 14 changed files with +2,024 and −42
added
benchmarks/synthetic/generators.py
+129 −0
@@ -0,0 +1,129 @@ | ||
| 1 | +# ============================================================================= | |
| 2 | +# Project : anomaly-atlas | |
| 3 | +# File : benchmarks/synthetic/generators.py | |
| 4 | +# Purpose : Synthetic series with KNOWN properties — the "test the tests" set | |
| 5 | +# Author : Simon-Pierre Boucher | |
| 6 | +# Contact : contact@spboucher.ai | |
| 7 | +# Data src : hfmarketdata.io (sole data source) | |
| 8 | +# Created : 2026-08-12 | |
| 9 | +# Modified : 2026-08-12 | |
| 10 | +# Platform : macOS / Apple Silicon (arm64) | |
| 11 | +# License : All rights reserved (research code) | |
| 12 | +# ============================================================================= | |
| 13 | +"""Synthetic price/return series with planted, analytically-known properties. | |
| 14 | + | |
| 15 | +Charter §8.1: before any detector touches real data it must (a) find NOTHING | |
| 16 | +in a pure random walk, (b) recover every planted effect, and (c) flag a pure | |
| 17 | +bid-ask-bounce series as an artifact, not an anomaly. These generators are the | |
| 18 | +ground truth for that gate (see test_synthetic_gate.py). | |
| 19 | + | |
| 20 | +All series are generated from an explicit seed; no global RNG state. | |
| 21 | +""" | |
| 22 | + | |
| 23 | +from __future__ import annotations | |
| 24 | + | |
| 25 | +import numpy as np | |
| 26 | + | |
| 27 | + | |
| 28 | +def random_walk(n: int, sigma: float = 0.001, seed: int = 0) -> np.ndarray: | |
| 29 | + """Pure log-price random walk. Ground truth: VR(q)=1, AC1(returns)=0.""" | |
| 30 | + rng = np.random.default_rng(seed) | |
| 31 | + return np.cumsum(rng.normal(0.0, sigma, n)) | |
| 32 | + | |
| 33 | + | |
| 34 | +def ou_prices(n: int, kappa: float, sigma: float = 0.001, seed: int = 0) -> np.ndarray: | |
| 35 | + """Mean-reverting (Ornstein-Uhlenbeck) log-price around 0. | |
| 36 | + | |
| 37 | + Discrete: p_t = (1 - kappa) * p_{t-1} + eps. Ground truth half-life | |
| 38 | + = ln(2) / -ln(1 - kappa); return AC1 < 0; VR(q) < 1 for q >= 2. | |
| 39 | + """ | |
| 40 | + rng = np.random.default_rng(seed) | |
| 41 | + p = np.empty(n) | |
| 42 | + p[0] = 0.0 | |
| 43 | + eps = rng.normal(0.0, sigma, n) | |
| 44 | + for t in range(1, n): | |
| 45 | + p[t] = (1.0 - kappa) * p[t - 1] + eps[t] | |
| 46 | + return p | |
| 47 | + | |
| 48 | + | |
| 49 | +def roll_bounce_prices(n: int, spread: float, sigma: float = 0.001, seed: int = 0) -> np.ndarray: | |
| 50 | + """Roll (1984) model: observed log-price = random-walk mid ± spread/2. | |
| 51 | + | |
| 52 | + Ground truth: Cov(r_t, r_{t-1}) = -spread^2/4, implied Roll spread = | |
| 53 | + 2*sqrt(-cov) = spread, and the WHOLE negative AC1 is artifact. | |
| 54 | + """ | |
| 55 | + rng = np.random.default_rng(seed) | |
| 56 | + mid = np.cumsum(rng.normal(0.0, sigma, n)) | |
| 57 | + q = rng.choice([-1.0, 1.0], size=n) | |
| 58 | + return mid + (spread / 2.0) * q | |
| 59 | + | |
| 60 | + | |
| 61 | +def leadlag_pair( | |
| 62 | + n: int, beta: float, lag: int, sigma: float = 0.001, seed: int = 0 | |
| 63 | +) -> tuple[np.ndarray, np.ndarray]: | |
| 64 | + """Return series (x, y) where x truly leads y by `lag` steps. | |
| 65 | + | |
| 66 | + y_t = beta * x_{t-lag} + noise. Ground truth: cross-corr peaks at `lag` | |
| 67 | + with corr ≈ beta*sd(x)/sd(y); zero at all other lags. | |
| 68 | + """ | |
| 69 | + rng = np.random.default_rng(seed) | |
| 70 | + x = rng.normal(0.0, sigma, n) | |
| 71 | + noise = rng.normal(0.0, sigma, n) | |
| 72 | + y = noise.copy() | |
| 73 | + y[lag:] += beta * x[: n - lag] | |
| 74 | + return x, y | |
| 75 | + | |
| 76 | + | |
| 77 | +def seasonal_returns( | |
| 78 | + n: int, | |
| 79 | + period: int, | |
| 80 | + hot_phase: int, | |
| 81 | + amplitude: float, | |
| 82 | + sigma: float = 0.001, | |
| 83 | + seed: int = 0, | |
| 84 | +) -> np.ndarray: | |
| 85 | + """Returns with a planted calendar effect: mean = amplitude on one phase. | |
| 86 | + | |
| 87 | + Ground truth: mean(returns | t % period == hot_phase) = amplitude, | |
| 88 | + all other phases 0. | |
| 89 | + """ | |
| 90 | + rng = np.random.default_rng(seed) | |
| 91 | + r = rng.normal(0.0, sigma, n) | |
| 92 | + r[np.arange(n) % period == hot_phase] += amplitude | |
| 93 | + return r | |
| 94 | + | |
| 95 | + | |
| 96 | +def stale_observe( | |
| 97 | + prices: np.ndarray, p_observe: float, seed: int = 0 | |
| 98 | +) -> tuple[np.ndarray, np.ndarray]: | |
| 99 | + """Simulate an illiquid ticker: each price prints with prob p_observe, | |
| 100 | + otherwise the last print is carried forward (LOCF). | |
| 101 | + | |
| 102 | + Returns (locf_prices, observed_mask). Ground truth: LOCF returns of a | |
| 103 | + random walk gain SPURIOUS positive lag-1 autocorrelation, and a fully | |
| 104 | + observed correlated series appears to LEAD the stale one. | |
| 105 | + """ | |
| 106 | + rng = np.random.default_rng(seed) | |
| 107 | + observed = rng.random(len(prices)) < p_observe | |
| 108 | + observed[0] = True | |
| 109 | + locf = prices.copy() | |
| 110 | + for t in range(1, len(prices)): | |
| 111 | + if not observed[t]: | |
| 112 | + locf[t] = locf[t - 1] | |
| 113 | + return locf, observed | |
| 114 | + | |
| 115 | + | |
| 116 | +def correlated_pair( | |
| 117 | + n: int, rho: float, sigma: float = 0.001, seed: int = 0 | |
| 118 | +) -> tuple[np.ndarray, np.ndarray]: | |
| 119 | + """Two random-walk log-prices with contemporaneously correlated innovations. | |
| 120 | + | |
| 121 | + Ground truth: corr(r_x, r_y) = rho at lag 0, zero at every nonzero lag — | |
| 122 | + any measured lead-lag after LOCF is pure artifact. | |
| 123 | + """ | |
| 124 | + rng = np.random.default_rng(seed) | |
| 125 | + z1 = rng.normal(0.0, sigma, n) | |
| 126 | + z2 = rng.normal(0.0, sigma, n) | |
| 127 | + rx = z1 | |
| 128 | + ry = rho * z1 + np.sqrt(1.0 - rho**2) * z2 | |
| 129 | + return np.cumsum(rx), np.cumsum(ry) | |
added
benchmarks/synthetic/test_synthetic_gate.py
+176 −0
@@ -0,0 +1,176 @@ | ||
| 1 | +# ============================================================================= | |
| 2 | +# Project : anomaly-atlas | |
| 3 | +# File : benchmarks/synthetic/test_synthetic_gate.py | |
| 4 | +# Purpose : §8.1 gate — detectors must pass synthetic ground truth first | |
| 5 | +# Author : Simon-Pierre Boucher | |
| 6 | +# Contact : contact@spboucher.ai | |
| 7 | +# Data src : hfmarketdata.io (sole data source) | |
| 8 | +# Created : 2026-08-12 | |
| 9 | +# Modified : 2026-08-12 | |
| 10 | +# Platform : macOS / Apple Silicon (arm64) | |
| 11 | +# License : All rights reserved (research code) | |
| 12 | +# ============================================================================= | |
| 13 | +"""The mandatory gate of charter §8.1, as executable tests: | |
| 14 | + | |
| 15 | + 1. pure random walk -> NO anomaly may be detected | |
| 16 | + 2. planted mean-reversion -> must be recovered (incl. half-life) | |
| 17 | + 3. planted lead-lag -> must be recovered at the right lag | |
| 18 | + 4. planted calendar effect -> must be recovered on the right phase | |
| 19 | + 5. pure bid-ask bounce -> must be flagged as ARTIFACT, not anomaly | |
| 20 | + 6. staleness (LOCF) -> must manufacture the documented artifacts | |
| 21 | + | |
| 22 | +A detector that fails any of these is broken and must not touch real data. | |
| 23 | +Multi-seed checks use fixed seed lists — fully deterministic. | |
| 24 | +""" | |
| 25 | + | |
| 26 | +from __future__ import annotations | |
| 27 | + | |
| 28 | +import sys | |
| 29 | +from pathlib import Path | |
| 30 | + | |
| 31 | +import numpy as np | |
| 32 | + | |
| 33 | +sys.path.insert(0, str(Path(__file__).resolve().parent)) | |
| 34 | + | |
| 35 | +from generators import ( # noqa: E402 | |
| 36 | + correlated_pair, | |
| 37 | + leadlag_pair, | |
| 38 | + ou_prices, | |
| 39 | + random_walk, | |
| 40 | + roll_bounce_prices, | |
| 41 | + seasonal_returns, | |
| 42 | + stale_observe, | |
| 43 | +) | |
| 44 | + | |
| 45 | +from anomaly_atlas.stats.bootstrap import moving_block_bootstrap, percentile_ci | |
| 46 | +from anomaly_atlas.stats.leadlag import lagged_xcorr, leadlag_asymmetry, peak_lag | |
| 47 | +from anomaly_atlas.stats.reversion import ac1, half_life, variance_ratio | |
| 48 | +from anomaly_atlas.validation.artifacts import ( | |
| 49 | + excess_reversion, | |
| 50 | + locf_fill, | |
| 51 | + roll_spread, | |
| 52 | + staleness_ratio, | |
| 53 | +) | |
| 54 | + | |
| 55 | +N = 100_000 | |
| 56 | +SEEDS = [1, 2, 3, 4, 5] | |
| 57 | + | |
| 58 | + | |
| 59 | +# ----------------------------------------------------- 1. random walk: nothing | |
| 60 | +def 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.02 | |
| 64 | + assert abs(variance_ratio(r, 5) - 1.0) < 0.05 | |
| 65 | + assert abs(variance_ratio(r, 30) - 1.0) < 0.12 | |
| 66 | + assert half_life(random_walk(N, seed=seed)) > 5_000 # effectively none | |
| 67 | + | |
| 68 | + | |
| 69 | +def 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 detection | |
| 74 | + | |
| 75 | + | |
| 76 | +def 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.02 | |
| 82 | + assert abs(leadlag_asymmetry(xc)) < 0.05 | |
| 83 | + | |
| 84 | + | |
| 85 | +# ------------------------------------------- 2. planted reversion is recovered | |
| 86 | +def test_ou_reversion_recovered_with_half_life(): | |
| 87 | + kappa = 0.02 # true half-life = ln2 / -ln(0.98) ≈ 34.3 bars | |
| 88 | + 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.005 | |
| 93 | + assert variance_ratio(r, 30) < 0.9 | |
| 94 | + assert abs(half_life(p) - true_hl) / true_hl < 0.25 | |
| 95 | + | |
| 96 | + | |
| 97 | +# --------------------------------------------- 3. planted lead-lag is recovered | |
| 98 | +def 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) == 2 | |
| 103 | + assert xc[2] > 0.2 | |
| 104 | + assert abs(xc[1]) < 0.02 and abs(xc[3]) < 0.02 | |
| 105 | + | |
| 106 | + | |
| 107 | +# --------------------------------------- 4. planted calendar effect is recovered | |
| 108 | +def test_planted_seasonal_effect_recovered_on_right_phase(): | |
| 109 | + period, hot, amp = 5, 3, 0.0005 | |
| 110 | + 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) == hot | |
| 114 | + assert abs(phase_means[hot] - amp) < amp * 0.2 | |
| 115 | + rest = [m for k, m in enumerate(phase_means) if k != hot] | |
| 116 | + assert max(abs(m) for m in rest) < amp * 0.2 | |
| 117 | + | |
| 118 | + | |
| 119 | +# ------------------------------------- 5. pure bounce is an artifact, not a find | |
| 120 | +def test_roll_spread_recovers_planted_spread(): | |
| 121 | + spread = 0.002 | |
| 122 | + 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.10 | |
| 126 | + | |
| 127 | + | |
| 128 | +def test_pure_bounce_reversion_vanishes_after_artifact_adjustment(): | |
| 129 | + spread = 0.002 | |
| 130 | + 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 ~0 | |
| 135 | + assert abs(excess_reversion(r, spread)) < 0.03 | |
| 136 | + | |
| 137 | + | |
| 138 | +def test_true_reversion_survives_artifact_adjustment(): | |
| 139 | + # OU + bounce: after removing the bounce share, reversion must REMAIN | |
| 140 | + kappa, spread = 0.05, 0.001 | |
| 141 | + 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.01 | |
| 147 | + | |
| 148 | + | |
| 149 | +# ------------------------------------------------ 6. staleness manufactures lies | |
| 150 | +def 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: nothing | |
| 156 | + # LOCF returns of a pure walk: AC1 pushed NEGATIVE at lag 1 grid steps | |
| 157 | + # is not the failure mode; the artifact is CROSS-serial (next test) and | |
| 158 | + # a big mass of zero returns. Document the zero-mass here: | |
| 159 | + assert (r == 0).mean() > 0.5 | |
| 160 | + assert staleness_ratio(mask) > 0.6 | |
| 161 | + | |
| 162 | + | |
| 163 | +def 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 0 | |
| 167 | + xc_true = lagged_xcorr(np.diff(px), np.diff(py), 3) | |
| 168 | + assert abs(xc_true[1]) < 0.02 | |
| 169 | + # y observed sparsely, LOCF-joined on the grid: x now "leads" y | |
| 170 | + mask = np.random.default_rng(seed).random(N) < 0.3 | |
| 171 | + mask[0] = True | |
| 172 | + 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 series | |
| 175 | + assert leadlag_asymmetry(xc) > 0.1 | |
| 176 | + assert peak_lag({k: v for k, v in xc.items() if k != 0}) == 1 | |
modified
data_manifest/index.jsonl
+47 −0
@@ -52,3 +52,50 @@ | ||
| 52 | 52 | {"author": "Simon-Pierre Boucher", "cache_key": "1f72e7b9a118cc59ee59baafdfd5ad65", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/MSFT", "fetched_utc": "2026-08-12T05:44:40Z", "first": "2025-10-01", "last": "2025-10-03", "params": {"adjustment": "adj_splitdiv", "end": "2025-10-05", "start": "2025-10-01", "timeframe": "1day"}, "rows": 3, "sha256": "54892bb0c9a6bda2b3e56338602d65e3f281d6ba1807028ac60a66352ea04edf"} |
| 53 | 53 | {"author": "Simon-Pierre Boucher", "cache_key": "98078fa812ec7189fd243149762722aa", "data_source": "hfmarketdata.io", "endpoint": "/v1/options/expirations/SPY", "fetched_utc": "2026-08-12T05:44:40Z", "first": null, "last": null, "params": {"trade_date": "2026-06-15"}, "rows": null, "sha256": "84ddfee82f215136f022f1b375895b964894f516d732dfb8f85baa3ab5b83fbc"} |
| 54 | 54 | {"author": "Simon-Pierre Boucher", "cache_key": "b2ce81ddb1aa764e164205ca08aec02d", "data_source": "hfmarketdata.io", "endpoint": "/v1/options/chain/SPY", "fetched_utc": "2026-08-12T05:44:40Z", "first": "2026-06-15", "last": "2026-06-15", "params": {"limit": 2, "trade_date": "2026-06-15"}, "rows": 2, "sha256": "cc63767f8abc43b71f5618e19bfd92b725e9b377d7aa9d828e05bd133d426d62"} |
| 55 | +{"author": "Simon-Pierre Boucher", "cache_key": "57193251505095d115d9821258d84799", "data_source": "hfmarketdata.io", "endpoint": "/v1/stock/tickers", "fetched_utc": "2026-08-12T05:56:02Z", "first": null, "last": null, "params": {"adjustment": "adj_split", "limit": 10000, "timeframe": "1min"}, "rows": null, "sha256": "032cb0a1f5518faf65f87e86397aa1d65c746c0fed24bae4fe4e025276c274ef"} | |
| 56 | +{"author": "Simon-Pierre Boucher", "cache_key": "6da965ad3c79e7a0875bf8aaaff66203", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/AAPL", "fetched_utc": "2026-08-12T05:56:03Z", "first": "2024-01-02 04:00:00", "last": "2024-03-28 19:59:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 47013, "sha256": "c3798cec4e63e79a0b3757adaa5262b0297d6f21f6e231e6a40c52e0f2a917f9"} | |
| 57 | +{"author": "Simon-Pierre Boucher", "cache_key": "d26fad0ebd498f478da02af5e937eab6", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/MSFT", "fetched_utc": "2026-08-12T05:56:04Z", "first": "2024-01-02 04:02:00", "last": "2024-03-28 19:59:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 39284, "sha256": "351766261c7200865ca73ee5493a33ec0861d7a49419cff7e457707a8360cda8"} | |
| 58 | +{"author": "Simon-Pierre Boucher", "cache_key": "b47ded4777110c562a0ea43f8a2429dc", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/NVDA", "fetched_utc": "2026-08-12T05:56:06Z", "first": "2024-01-02 04:00:00", "last": "2024-03-25 16:07:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 50000, "sha256": "941d8e1258b6d4963432600d39b67f06e52f975b8b5b4da6139841ec4be2dfb2"} | |
| 59 | +{"author": "Simon-Pierre Boucher", "cache_key": "0b343a86c050098a0bc59fd89c6a2e5e", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/NVDA", "fetched_utc": "2026-08-12T05:56:06Z", "first": "2024-03-25 16:07:00", "last": "2024-03-28 19:59:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-03-25 16:07:00", "timeframe": "1min"}, "rows": 2686, "sha256": "ac6970d4679dbfc05d1af5ea93e29f1558571f5d7b1168dd1b56677d9a2efc1b"} | |
| 60 | +{"author": "Simon-Pierre Boucher", "cache_key": "9e2b82de1e467b9ce2a1e4513b62f8d3", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/AMZN", "fetched_utc": "2026-08-12T05:56:08Z", "first": "2024-01-02 04:00:00", "last": "2024-03-28 19:59:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 44415, "sha256": "cee6532b43e2ed9475b73c89a976d7dcb6cb2685b5cfa05b87096a1a78da4daa"} | |
| 61 | +{"author": "Simon-Pierre Boucher", "cache_key": "30562c78f93f7091bd62f37ac799b37c", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/GOOGL", "fetched_utc": "2026-08-12T05:56:09Z", "first": "2024-01-02 04:00:00", "last": "2024-03-28 19:59:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 40036, "sha256": "6f41850f3e631287e97059c9328974ae01c666eb02aa3b94e6730f5c53fc31f9"} | |
| 62 | +{"author": "Simon-Pierre Boucher", "cache_key": "4b7bf27e6c36d034e0ffc67710041bed", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/META", "fetched_utc": "2026-08-12T05:56:10Z", "first": "2024-01-02 04:07:00", "last": "2024-03-28 19:57:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 36679, "sha256": "c5490da0c6d966ecc57a9f1b127ff83c4107e0636c4595616105de1ad471e998"} | |
| 63 | +{"author": "Simon-Pierre Boucher", "cache_key": "1e20301ea13e362f0d1bd5f4dcbb7a28", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/TSLA", "fetched_utc": "2026-08-12T05:56:11Z", "first": "2024-01-02 04:00:00", "last": "2024-03-20 18:29:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 50000, "sha256": "5004c0c72c6ade5bda1c1ff2f6e7ecc41854befb71c8defebf43fc1e952c0492"} | |
| 64 | +{"author": "Simon-Pierre Boucher", "cache_key": "8af5eef323872e457d211808f77a73cb", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/TSLA", "fetched_utc": "2026-08-12T05:56:12Z", "first": "2024-03-20 18:29:00", "last": "2024-03-28 19:59:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-03-20 18:29:00", "timeframe": "1min"}, "rows": 5515, "sha256": "350519fbc28951461b623154c50ae14429686d97df0eef354181314afe8d9385"} | |
| 65 | +{"author": "Simon-Pierre Boucher", "cache_key": "13b78e911df698d059161bf82f97d698", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/JPM", "fetched_utc": "2026-08-12T05:56:13Z", "first": "2024-01-02 04:03:00", "last": "2024-03-28 19:46:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 26645, "sha256": "d1383a06a9c8f047d23ffd71e0fa5913b1fb71c06df6711f3143ff27fd844714"} | |
| 66 | +{"author": "Simon-Pierre Boucher", "cache_key": "4ae363aae108f48a4febf58acb28daf2", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/XOM", "fetched_utc": "2026-08-12T05:56:13Z", "first": "2024-01-02 04:00:00", "last": "2024-03-28 19:46:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 29547, "sha256": "f5ddcad08f5f5cb9f65d34e6925d2f2fd7a46ddf74bf37fdd3d6bc13d6996f50"} | |
| 67 | +{"author": "Simon-Pierre Boucher", "cache_key": "b873f50315fc2655ca598a0e2ddb9823", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/UNH", "fetched_utc": "2026-08-12T05:56:14Z", "first": "2024-01-02 08:00:00", "last": "2024-03-28 19:26:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 25046, "sha256": "1248f316966a11334ca8fe4db38b0153403ef8cc1ddf2535e0e751b97496d1df"} | |
| 68 | +{"author": "Simon-Pierre Boucher", "cache_key": "6728ecaa8020e9fa2802371e2213303c", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/etf/SPY", "fetched_utc": "2026-08-12T05:56:16Z", "first": "2024-01-02 04:00:00", "last": "2024-03-28 19:59:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 48128, "sha256": "81ccf6af42aee4933d8b1d935a715c0813a6c953092c58c9ca2c2823a72235d1"} | |
| 69 | +{"author": "Simon-Pierre Boucher", "cache_key": "fc436a19a56c751cfca0771b9697dbb2", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/etf/QQQ", "fetched_utc": "2026-08-12T05:56:17Z", "first": "2024-01-02 04:00:00", "last": "2024-03-26 15:21:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 50000, "sha256": "28eebbcfe1b74e92cb9699374066c63cb7e4e164de00fd2183933c39ce931461"} | |
| 70 | +{"author": "Simon-Pierre Boucher", "cache_key": "97754686f049c1e742200547469df859", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/etf/QQQ", "fetched_utc": "2026-08-12T05:56:17Z", "first": "2024-03-26 15:21:00", "last": "2024-03-28 19:59:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-03-26 15:21:00", "timeframe": "1min"}, "rows": 1834, "sha256": "6cc38c6f40bd383495f8c18250fe4f4107c52a0a8a1c17a34bd9fdb7d42a4b4e"} | |
| 71 | +{"author": "Simon-Pierre Boucher", "cache_key": "93a7adbc0ac6f334920476e4fe0b83ec", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/ATRO", "fetched_utc": "2026-08-12T05:56:18Z", "first": "2024-01-02 09:30:00", "last": "2024-03-28 16:00:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 7444, "sha256": "1a41d1281a9fd1cf1a5062abcdaecb8ebf0eb61ebc7c084471e420cf4e75e757"} | |
| 72 | +{"author": "Simon-Pierre Boucher", "cache_key": "238f080feba962d5bf39dd44016f0938", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/AUVI", "fetched_utc": "2026-08-12T05:56:18Z", "first": "2024-01-02 04:53:00", "last": "2024-03-28 19:34:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 10200, "sha256": "1d1cb461a53922fe481c8b8e984fb4ef53ec5e62dfee121f93a82eb079d9c191"} | |
| 73 | +{"author": "Simon-Pierre Boucher", "cache_key": "1d654a3efff19034bcb6f9aee5807410", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/AXDX", "fetched_utc": "2026-08-12T05:56:18Z", "first": "2024-01-02 08:00:00", "last": "2024-03-28 16:05:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 4142, "sha256": "fbc451ad92fce083b33efc0447692b6e88cb65f3a9209f8e3d6271577e29dfad"} | |
| 74 | +{"author": "Simon-Pierre Boucher", "cache_key": "d3b66988e16c0258b14b3e1b7fff1bb4", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/BKE", "fetched_utc": "2026-08-12T05:56:19Z", "first": "2024-01-02 09:30:00", "last": "2024-03-28 16:20:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 17146, "sha256": "93d22e4ec35d512177ef6cae24647b529df0d1c42326cf3b45a44e3592f542d1"} | |
| 75 | +{"author": "Simon-Pierre Boucher", "cache_key": "1b8bc68e1d2199ccfe83459e85cd9f61", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/CECO", "fetched_utc": "2026-08-12T05:56:19Z", "first": "2024-01-02 09:30:00", "last": "2024-03-28 16:00:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 11324, "sha256": "a4fcc21e6b6f5b58f4226b4f5eb0d9dd34c0b01ade2f622a7428b977a151ce6d"} | |
| 76 | +{"author": "Simon-Pierre Boucher", "cache_key": "8aae75bdf25ad09ca9c339b85068537d", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/CKX", "fetched_utc": "2026-08-12T05:56:19Z", "first": "2024-01-02 12:17:00", "last": "2024-03-28 15:57:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 241, "sha256": "10486e8127e6a78f1f750b18aee073f5b3d6d65b532b25874e46278d3d929b79"} | |
| 77 | +{"author": "Simon-Pierre Boucher", "cache_key": "3ff2dc4c9bacfb07bfa39ea364085d78", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/GCL", "fetched_utc": "2026-08-12T05:56:20Z", "first": "2024-01-11 10:06:00", "last": "2024-03-25 11:47:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 44, "sha256": "1bf5b2ff559f35089c452dbd64c338478a77c3a469ab3799f25b12143ee04679"} | |
| 78 | +{"author": "Simon-Pierre Boucher", "cache_key": "5e109c654808ac673f4ac3a4d06f6aaf", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/GRO", "fetched_utc": "2026-08-12T05:56:20Z", "first": null, "last": null, "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 0, "sha256": "8062687d3dbe7963af9b2fa82e28bdbf5ce3aee72af33fa08113f6e5bf11df4e"} | |
| 79 | +{"author": "Simon-Pierre Boucher", "cache_key": "501c5e8944e9134f777351d9b7817327", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/HPE", "fetched_utc": "2026-08-12T05:56:21Z", "first": "2024-01-02 04:05:00", "last": "2024-03-28 16:22:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 26409, "sha256": "45cceca84f2bf737c7608d3639e659f948d1eb67262d9604fc7e16ce43945200"} | |
| 80 | +{"author": "Simon-Pierre Boucher", "cache_key": "1aa56bb61edd614ee1c2bb3029c7292a", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/HTD", "fetched_utc": "2026-08-12T05:56:21Z", "first": "2024-01-02 09:30:00", "last": "2024-03-28 16:00:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 7436, "sha256": "b2d26e1cc068af78ee634ef0141e9a816b49aabc31b51e963ff5aa16c207768e"} | |
| 81 | +{"author": "Simon-Pierre Boucher", "cache_key": "33aca3fe78b93c15bdbace2bf197f3f8", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/HWM", "fetched_utc": "2026-08-12T05:56:22Z", "first": "2024-01-02 08:00:00", "last": "2024-03-28 16:20:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 24023, "sha256": "ea220462ee70fa064a7753ef889fbac66c18e4f5ca5f4a6665f81934eea6bae4"} | |
| 82 | +{"author": "Simon-Pierre Boucher", "cache_key": "4e4c68c953dfacd48f8c62cb8b5bbaa7", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/ICUI", "fetched_utc": "2026-08-12T05:56:22Z", "first": "2024-01-02 09:30:00", "last": "2024-03-28 16:02:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 12133, "sha256": "c2c1952a0879b2c87aaf3941f4a9d39170d972acab877cd7873fcd07c396cf2a"} | |
| 83 | +{"author": "Simon-Pierre Boucher", "cache_key": "bd01b93fc349c1cbcf84303066980cd3", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/KEY.K", "fetched_utc": "2026-08-12T05:56:22Z", "first": "2024-01-02 09:44:00", "last": "2024-03-28 16:00:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 3202, "sha256": "fb5a39b256ddb08eb23883deacbfbf7b017673c32140bf879997b0f732e68341"} | |
| 84 | +{"author": "Simon-Pierre Boucher", "cache_key": "c6e25c9763e17026cf5e77242c4ddb14", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/KSPI", "fetched_utc": "2026-08-12T05:56:23Z", "first": "2024-01-22 06:55:00", "last": "2024-03-28 16:00:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 10172, "sha256": "13ee966b9e0c91db8c60f24ddb8205c5595ae8901b34cee8a63fac7c6af547d2"} | |
| 85 | +{"author": "Simon-Pierre Boucher", "cache_key": "9df1af4190dd161b545d783203a933da", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/LENZ", "fetched_utc": "2026-08-12T05:56:23Z", "first": "2024-01-02 08:30:00", "last": "2024-03-28 16:04:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 8308, "sha256": "ae3031a1f694e859277b4c254a9d857ec1fcae02d2050907809522c89842647e"} | |
| 86 | +{"author": "Simon-Pierre Boucher", "cache_key": "71d2445f7392860a6f37c0412144d279", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/LTSL", "fetched_utc": "2026-08-12T05:56:23Z", "first": "2024-01-08 11:41:00", "last": "2024-03-28 15:36:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 122, "sha256": "3b57c6a65eaab5260245399d7c1ec2fcb12558028cf147bbf9f6bfe20d277d08"} | |
| 87 | +{"author": "Simon-Pierre Boucher", "cache_key": "8cc13085f63434268be94cd37b1babb7", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/NSTS", "fetched_utc": "2026-08-12T05:56:23Z", "first": "2024-01-03 15:00:00", "last": "2024-03-28 16:00:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 255, "sha256": "aedfb55142adb7b8de418eb36a3288c705c6c9b8156eebdb4fff72823e993c8e"} | |
| 88 | +{"author": "Simon-Pierre Boucher", "cache_key": "df097b931f431d8b8899af488b02f0b6", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/NWAX", "fetched_utc": "2026-08-12T05:56:24Z", "first": null, "last": null, "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 0, "sha256": "8062687d3dbe7963af9b2fa82e28bdbf5ce3aee72af33fa08113f6e5bf11df4e"} | |
| 89 | +{"author": "Simon-Pierre Boucher", "cache_key": "688b80931b78ad8c6acfbac669cd1a7f", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/PBYI", "fetched_utc": "2026-08-12T05:56:24Z", "first": "2024-01-02 08:11:00", "last": "2024-03-28 17:19:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 16569, "sha256": "9a1044c49f888f7cae2dc495fca415722072d49f319e9af861fa105a3c42e875"} | |
| 90 | +{"author": "Simon-Pierre Boucher", "cache_key": "075ac2c34491c2a583c1b6aac079ea33", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/PLNT", "fetched_utc": "2026-08-12T05:56:25Z", "first": "2024-01-02 07:55:00", "last": "2024-03-28 16:30:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 23291, "sha256": "3b5b52266a56332281ef275d5569740c806df91403897438361dbcd580f45fc1"} | |
| 91 | +{"author": "Simon-Pierre Boucher", "cache_key": "19479bedf5c7a972d44a1e687895ce5b", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/PSA.G", "fetched_utc": "2026-08-12T05:56:25Z", "first": "2024-01-02 09:30:00", "last": "2024-03-28 16:00:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 1581, "sha256": "ea85a2704b7ecde1c408af8e4da8412784ba04251648b699f206967c8a809a95"} | |
| 92 | +{"author": "Simon-Pierre Boucher", "cache_key": "9d63538e8455fa3108091b893d722950", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/RDZN", "fetched_utc": "2026-08-12T05:56:25Z", "first": "2024-01-02 08:00:00", "last": "2024-03-28 15:56:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 955, "sha256": "c918edc2142d679bbd880ce88ae1b4fe6d5f477349f3276066694c86bce84352"} | |
| 93 | +{"author": "Simon-Pierre Boucher", "cache_key": "0ec4f590a02d68cf7d0337e89f3a1baa", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/RITM.B", "fetched_utc": "2026-08-12T05:56:26Z", "first": "2024-01-02 09:40:00", "last": "2024-03-28 16:00:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 2447, "sha256": "7a893130a1e57329626bfb66a6a9f5481811fa18732b7d08f845b7d37d1c1bc1"} | |
| 94 | +{"author": "Simon-Pierre Boucher", "cache_key": "853e4a2cef1474fafec4e5a3b889cb3e", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/RPM", "fetched_utc": "2026-08-12T05:56:26Z", "first": "2024-01-02 09:30:00", "last": "2024-03-28 16:20:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 18682, "sha256": "6b6eb4d7ebce44fb5658b21bf61fdaf2d8e80750e846ec4d73faddc43b9c216e"} | |
| 95 | +{"author": "Simon-Pierre Boucher", "cache_key": "7a9cc362158bc889c5a511f7582f4276", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/RUM", "fetched_utc": "2026-08-12T05:56:27Z", "first": "2024-01-02 07:01:00", "last": "2024-03-28 19:41:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 30567, "sha256": "59008c590811876cf576c6f09ce2cf94a539a01a26747badfed717e4c20a9dc1"} | |
| 96 | +{"author": "Simon-Pierre Boucher", "cache_key": "6020217db7c98bfe1a82708406e50369", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/SLF", "fetched_utc": "2026-08-12T05:56:28Z", "first": "2024-01-02 09:30:00", "last": "2024-03-28 16:00:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 19536, "sha256": "20cfc5f4ee1b20c6336a19a1a895309750f06e12ec18dd92148e38f7640abbd5"} | |
| 97 | +{"author": "Simon-Pierre Boucher", "cache_key": "f38e9c35f15ad96299c978a3acb5e068", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/SPB", "fetched_utc": "2026-08-12T05:56:28Z", "first": "2024-01-02 09:30:00", "last": "2024-03-28 16:05:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 15947, "sha256": "a78aed00d065b23860d4019cf031f02a88c217e2f546e14079be65b978334275"} | |
| 98 | +{"author": "Simon-Pierre Boucher", "cache_key": "5d5e9ea1f442098aaf202a1c87d69c54", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/STRRP", "fetched_utc": "2026-08-12T05:56:28Z", "first": "2024-01-03 14:26:00", "last": "2024-03-26 15:28:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 174, "sha256": "9d3918a87642c2bc1086d37ddf0c79a1c13c959f3b88de55e9d4ed01e1591423"} | |
| 99 | +{"author": "Simon-Pierre Boucher", "cache_key": "a2d1fc0ce26f55ac2dbcb8a56f027e70", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/USGOW", "fetched_utc": "2026-08-12T05:56:29Z", "first": "2024-01-02 10:52:00", "last": "2024-03-28 15:52:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 306, "sha256": "1c4689daece65d79483cc48b51dc7452ceaaaef134cdbce98cde5a97acbe4dc5"} | |
| 100 | +{"author": "Simon-Pierre Boucher", "cache_key": "6641c48ba15e0036b7af55422eb3e0f8", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/stock/WTFCM", "fetched_utc": "2026-08-12T05:56:29Z", "first": "2024-01-02 09:30:00", "last": "2024-03-28 16:00:00", "params": {"adjustment": "adj_split", "end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 1545, "sha256": "85e69461dee2e1efb17b457d5770b7a9b4dc47695345fc3789f8ea3f8a6fecae"} | |
| 101 | +{"author": "Simon-Pierre Boucher", "cache_key": "a13526fe4efaf465254003732bca9ecf", "data_source": "hfmarketdata.io", "endpoint": "/v1/bars/index/SPX", "fetched_utc": "2026-08-12T05:56:30Z", "first": "2024-01-02 09:30:00", "last": "2024-03-28 16:05:00", "params": {"end": "2024-04-01", "limit": 50000, "order": "asc", "start": "2024-01-02", "timeframe": "1min"}, "rows": 24156, "sha256": "24aeff152902c703c832f91ba7ff331776643cc8cbaf9b9d36194f846736ade4"} | |
modified
experiments/micro/expB_artifact_baselines/README.md
+1 −1
@@ -12,4 +12,4 @@ status: draft | ||
| 12 | 12 | |
| 13 | 13 | Artifact baselines: null distributions of bounce, staleness, non-synchronous lead-lag |
| 14 | 14 | |
| 15 | −Status: scaffolded 2026-08-12, not yet run. | |
| 15 | +Status: **completed 2026-08-12** — see analysis.md; null magnitudes recorded in research/artifact_taxonomy.md. | |
modified
experiments/micro/expB_artifact_baselines/analysis.md
+46 −2
@@ -5,9 +5,53 @@ author: Simon-Pierre Boucher | ||
| 5 | 5 | contact: contact@spboucher.ai |
| 6 | 6 | data_source: hfmarketdata.io |
| 7 | 7 | created: 2026-08-12 |
| 8 | −status: draft | |
| 8 | +modified: 2026-08-12 | |
| 9 | +status: reviewed | |
| 9 | 10 | --- |
| 10 | 11 | |
| 11 | 12 | # Analysis — expB_artifact_baselines |
| 12 | 13 | |
| 13 | −*To be written after results exist. Must include the seven-field block and the evidence standard of CLAUDE.md §10 (never report an in-sample number as a finding).* | |
| 14 | +Run: `results/expB_artifact_baselines/20260812T055602Z/results.json` | |
| 15 | +(hardware manifest embedded; 47 network requests, 795 185 rows, protocol | |
| 16 | +pre-specified in hypothesis.md; detectors passed the §8.1 synthetic gate — | |
| 17 | +11 tests — before touching this data). | |
| 18 | + | |
| 19 | +## The measured artifact nulls (Q1 2024, RTH 1min, 31 tickers) | |
| 20 | + | |
| 21 | +| Staleness tercile | median staleness | median AC1 | Roll rel. spread | VR(30) | SPY leads +1min | | |
| 22 | +|---|---|---|---|---|---| | |
| 23 | +| fresh | 0.000 | −0.009 | 1.4 bp | 0.972 | +0.005 | | |
| 24 | +| mid | 0.107 | −0.051 | 3.8 bp | 0.901 | +0.047 | | |
| 25 | +| stale | 0.691 | −0.232 | 17.0 bp | 0.547 | +0.021 | | |
| 26 | + | |
| 27 | +* **Bounce/staleness dominate naive reversion metrics.** With *zero* planted | |
| 28 | + economics, illiquid names show VR(30) = 0.55 and AC1 = −0.23 (extreme: | |
| 29 | + RITM.B, 90 % stale minutes, VR30 = 0.35). Any reversion scan that does not | |
| 30 | + clear these levels for its liquidity bucket is measuring market plumbing. | |
| 31 | +* **Mega-caps show no measurable bounce at 1min**: AAPL/SPY/NVDA AC1 CIs | |
| 32 | + cover 0 and the Roll estimator is undefined (positive lag-1 autocov) — | |
| 33 | + the bounce null is liquidity-dependent, not universal. | |
| 34 | +* **The stale-price lead-lag artifact is real and monotone**: SPY spuriously | |
| 35 | + "leads" tickers by +1 min in proportion to their staleness | |
| 36 | + (Spearman = +0.43). It peaks in the *mid* tercile (+0.047): the stalest | |
| 37 | + names trade so rarely that even LOCF correlation collapses — the artifact | |
| 38 | + is worst where it is least obvious. | |
| 39 | +* **SPX-vs-SPY**: contemporaneous corr 0.965, and a +0.065 cross-correlation | |
| 40 | + with SPY leading by 1 minute. An "ETF price discovery leads the index" | |
| 41 | + finding is manufactured by index print staleness — measured here so Q2 | |
| 42 | + hypotheses must beat it. | |
| 43 | + | |
| 44 | +## Limitations | |
| 45 | + | |
| 46 | +Level 0 by construction (descriptive nulls; single quarter; one venue's | |
| 47 | +bar convention). 11/42 tickers dropped for insufficient data — the null for | |
| 48 | +*ultra*-illiquid names is therefore understated. Q1-2024-specific levels; | |
| 49 | +expC should re-measure per period rather than reuse these constants blindly. | |
| 50 | + | |
| 51 | +## Verdict | |
| 52 | + | |
| 53 | +**Complete — nulls established and usable.** Both pre-specified | |
| 54 | +falsification criteria failed to trigger. Numbers are recorded in | |
| 55 | +`research/artifact_taxonomy.md`; expC (reversion scan) must report every | |
| 56 | +effect *net of* the bucket-matched bounce null, and expD must run the | |
| 57 | +synchronized-vs-raw timestamp comparison this experiment quantified. | |
modified
experiments/micro/expB_artifact_baselines/benchmark.py
+245 −10
@@ -1,7 +1,7 @@ | ||
| 1 | 1 | # ============================================================================= |
| 2 | 2 | # Project : anomaly-atlas |
| 3 | 3 | # File : experiments/micro/expB_artifact_baselines/benchmark.py |
| 4 | −# Purpose : Benchmark runner: Artifact baselines: null distributions of bounce, staleness, non… | |
| 4 | +# Purpose : Measure the artifact nulls: bounce, staleness, LOCF lead-lag | |
| 5 | 5 | # Author : Simon-Pierre Boucher |
| 6 | 6 | # Contact : contact@spboucher.ai |
| 7 | 7 | # Data src : hfmarketdata.io (sole data source) |
@@ -10,25 +10,260 @@ | ||
| 10 | 10 | # Platform : macOS / Apple Silicon (arm64) |
| 11 | 11 | # License : All rights reserved (research code) |
| 12 | 12 | # ============================================================================= |
| 13 | +"""Experiment B — artifact baselines on real data (pre-specified protocol in | |
| 14 | +hypothesis.md; detectors gated on synthetic ground truth first, §8.1). | |
| 13 | 15 | |
| 14 | −"""Benchmark entry point for expB_artifact_baselines. | |
| 15 | − | |
| 16 | −Must embed the hardware manifest in all result output | |
| 17 | −(see benchmarks/hardware_manifest.py) and write results to | |
| 18 | −results/expB_artifact_baselines/<timestamp>/. Uses hfmarketdata.io data ONLY, exclusively | |
| 19 | −through src/anomaly_atlas/data/hf_client.py. | |
| 16 | +Every number produced here is a NULL LEVEL (Level 0 by construction): the | |
| 17 | +fake-signal magnitude that later experiments must exceed before claiming | |
| 18 | +anything. Universe, window, seeds are pre-specified; all data flows through | |
| 19 | +the cached hf_client. | |
| 20 | 20 | """ |
| 21 | 21 | |
| 22 | +from __future__ import annotations | |
| 23 | + | |
| 24 | +import json | |
| 22 | 25 | import sys |
| 26 | +from datetime import UTC, datetime | |
| 23 | 27 | from pathlib import Path |
| 24 | 28 | |
| 25 | −sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "benchmarks")) | |
| 29 | +import numpy as np | |
| 30 | + | |
| 31 | +REPO_ROOT = Path(__file__).resolve().parents[3] | |
| 32 | +sys.path.insert(0, str(REPO_ROOT / "benchmarks")) | |
| 33 | +sys.path.insert(0, str(REPO_ROOT / "src")) | |
| 34 | + | |
| 26 | 35 | from hardware_manifest import collect_manifest # noqa: E402 |
| 27 | 36 | |
| 37 | +from anomaly_atlas.data.hf_client import HFMarketDataClient # noqa: E402 | |
| 38 | +from anomaly_atlas.stats.bootstrap import moving_block_bootstrap, percentile_ci # noqa: E402 | |
| 39 | +from anomaly_atlas.stats.reversion import ac1, variance_ratio # noqa: E402 | |
| 40 | +from anomaly_atlas.validation.artifacts import roll_spread # noqa: E402 | |
| 41 | + | |
| 42 | +LIQUID_STOCK = ["AAPL", "MSFT", "NVDA", "AMZN", "GOOGL", "META", "TSLA", "JPM", "XOM", "UNH"] | |
| 43 | +LIQUID_ETF = ["SPY", "QQQ"] | |
| 44 | +N_RANDOM, RANDOM_SEED = 30, 42 | |
| 45 | +START, END = "2024-01-02", "2024-04-01" | |
| 46 | +ADJ = "adj_split" | |
| 47 | +BOOT_N, BOOT_SEED = 300, 42 | |
| 48 | +MAX_LAG = 3 | |
| 49 | + | |
| 50 | +RTH_MINUTES = [f"{h:02d}:{m:02d}" for h in range(9, 16) for m in range(60)] | |
| 51 | +RTH_MINUTES = [t for t in RTH_MINUTES if "09:30" <= t < "16:00"] # 390 slots | |
| 52 | +SLOT = {t: i for i, t in enumerate(RTH_MINUTES)} | |
| 53 | + | |
| 54 | + | |
| 55 | +def rth_day_grids(bars: list[dict]) -> dict[str, np.ndarray]: | |
| 56 | + """day -> 390-slot array of log close prices (NaN where no print).""" | |
| 57 | + days: dict[str, np.ndarray] = {} | |
| 58 | + for b in bars: | |
| 59 | + dt = b["datetime"] | |
| 60 | + t = dt[11:16] | |
| 61 | + if not ("09:30" <= t < "16:00"): | |
| 62 | + continue | |
| 63 | + grid = days.setdefault(dt[:10], np.full(390, np.nan)) | |
| 64 | + grid[SLOT[t]] = np.log(b["close"]) | |
| 65 | + return days | |
| 66 | + | |
| 67 | + | |
| 68 | +def trade_time_returns(days: dict[str, np.ndarray]) -> np.ndarray: | |
| 69 | + """Within-day log returns between consecutive PRINTS (no grid, no LOCF).""" | |
| 70 | + out = [] | |
| 71 | + for day in sorted(days): | |
| 72 | + p = days[day] | |
| 73 | + obs = p[np.isfinite(p)] | |
| 74 | + if len(obs) >= 2: | |
| 75 | + out.append(np.diff(obs)) | |
| 76 | + return np.concatenate(out) if out else np.array([]) | |
| 77 | + | |
| 78 | + | |
| 79 | +def locf_grid_returns(days: dict[str, np.ndarray], day_list: list[str]) -> np.ndarray: | |
| 80 | + """Concatenated per-day LOCF grid returns, NaN before first print and at | |
| 81 | + day boundaries — the join that MANUFACTURES the stale-price artifact.""" | |
| 82 | + out = [] | |
| 83 | + for day in day_list: | |
| 84 | + p = days.get(day) | |
| 85 | + if p is None: | |
| 86 | + out.append(np.full(389, np.nan)) | |
| 87 | + continue | |
| 88 | + filled = p.copy() | |
| 89 | + for i in range(1, 390): | |
| 90 | + if not np.isfinite(filled[i]): | |
| 91 | + filled[i] = filled[i - 1] | |
| 92 | + out.append(np.diff(filled)) # NaN propagates before first print | |
| 93 | + return np.concatenate(out) | |
| 94 | + | |
| 95 | + | |
| 96 | +def nan_xcorr(x: np.ndarray, y: np.ndarray, max_lag: int) -> dict[int, float]: | |
| 97 | + """corr(x_{t-k}, y_t) over finite pairs only; k>0 = x leads y.""" | |
| 98 | + n = min(len(x), len(y)) | |
| 99 | + x, y = x[:n], y[:n] | |
| 100 | + out: dict[int, float] = {} | |
| 101 | + for k in range(-max_lag, max_lag + 1): | |
| 102 | + a = x[: n - k] if k >= 0 else x[-k:] | |
| 103 | + b = y[k:] if k >= 0 else y[: n + k] | |
| 104 | + m = np.isfinite(a) & np.isfinite(b) | |
| 105 | + if m.sum() < 100 or a[m].std() == 0 or b[m].std() == 0: | |
| 106 | + out[k] = float("nan") | |
| 107 | + continue | |
| 108 | + out[k] = float(np.corrcoef(a[m], b[m])[0, 1]) | |
| 109 | + return out | |
| 110 | + | |
| 111 | + | |
| 112 | +def analyze_ticker(days: dict[str, np.ndarray], day_list: list[str]) -> dict | None: | |
| 113 | + present = ( | |
| 114 | + np.concatenate([np.isfinite(days[d]) for d in day_list if d in days]) | |
| 115 | + if any(d in days for d in day_list) | |
| 116 | + else np.array([]) | |
| 117 | + ) | |
| 118 | + n_days_covered = sum(d in days for d in day_list) | |
| 119 | + if n_days_covered < 30: | |
| 120 | + return None | |
| 121 | + r = trade_time_returns(days) | |
| 122 | + if len(r) < 2_000: | |
| 123 | + return None | |
| 124 | + block = max(50, len(r) // max(n_days_covered, 1)) | |
| 125 | + boot = moving_block_bootstrap(r, ac1, block=block, n_boot=BOOT_N, seed=BOOT_SEED) | |
| 126 | + lo, hi = percentile_ci(boot) | |
| 127 | + spread = roll_spread(r) | |
| 128 | + return { | |
| 129 | + "days_covered": n_days_covered, | |
| 130 | + "staleness": round(1.0 - present.mean() * len(present) / (390 * n_days_covered), 4) | |
| 131 | + if n_days_covered | |
| 132 | + else None, | |
| 133 | + "rth_fill_ratio": round(present.sum() / (390 * n_days_covered), 4), | |
| 134 | + "n_trade_returns": int(len(r)), | |
| 135 | + "ac1": round(ac1(r), 5), | |
| 136 | + "ac1_ci95": [round(lo, 5), round(hi, 5)], | |
| 137 | + "roll_rel_spread": round(spread, 6) if np.isfinite(spread) else None, | |
| 138 | + "vr5": round(variance_ratio(r, 5), 4), | |
| 139 | + "vr30": round(variance_ratio(r, 30), 4), | |
| 140 | + } | |
| 141 | + | |
| 28 | 142 | |
| 29 | 143 | def main() -> None: |
| 30 | − collect_manifest() # embedded in results once implemented | |
| 31 | − raise NotImplementedError("experiment not yet implemented") | |
| 144 | + run_utc = datetime.now(UTC) | |
| 145 | + client = HFMarketDataClient() | |
| 146 | + | |
| 147 | + # deterministic random universe (seed pre-specified) | |
| 148 | + all_stock = client.tickers("stock", timeframe="1min", adjustment=ADJ) | |
| 149 | + rng = np.random.default_rng(RANDOM_SEED) | |
| 150 | + random_universe = sorted(rng.choice(sorted(all_stock), N_RANDOM, replace=False)) | |
| 151 | + universe = ( | |
| 152 | + [("stock", t, "liquid") for t in LIQUID_STOCK] | |
| 153 | + + [("etf", t, "liquid") for t in LIQUID_ETF] | |
| 154 | + + [("stock", t, "random") for t in random_universe] | |
| 155 | + ) | |
| 156 | + | |
| 157 | + # fetch + grid everything | |
| 158 | + grids: dict[str, dict[str, np.ndarray]] = {} | |
| 159 | + for asset, ticker, _ in universe: | |
| 160 | + bars = client.get_bars(asset, ticker, "1min", ADJ, START, END) | |
| 161 | + grids[ticker] = rth_day_grids(bars) | |
| 162 | + print(f"{ticker}: {sum(len(v[np.isfinite(v)]) for v in grids[ticker].values())} RTH bars") | |
| 163 | + day_list = sorted(grids["SPY"].keys()) # trading calendar := SPY days | |
| 164 | + | |
| 165 | + # B1-B3: per-ticker artifact levels | |
| 166 | + per_ticker: dict[str, dict] = {} | |
| 167 | + for asset, ticker, bucket in universe: | |
| 168 | + m = analyze_ticker(grids[ticker], day_list) | |
| 169 | + if m is not None: | |
| 170 | + m["bucket"] = bucket | |
| 171 | + m["asset"] = asset | |
| 172 | + per_ticker[ticker] = m | |
| 173 | + | |
| 174 | + # B4: LOCF lead-lag vs SPY | |
| 175 | + spy_r = locf_grid_returns(grids["SPY"], day_list) | |
| 176 | + for ticker, m in per_ticker.items(): | |
| 177 | + if ticker == "SPY": | |
| 178 | + continue | |
| 179 | + r = locf_grid_returns(grids[ticker], day_list) | |
| 180 | + xc = nan_xcorr(spy_r, r, MAX_LAG) | |
| 181 | + m["xcorr_vs_spy"] = {str(k): round(v, 5) if np.isfinite(v) else None for k, v in xc.items()} | |
| 182 | + m["spy_leads_+1"] = round(xc[1], 5) if np.isfinite(xc[1]) else None | |
| 183 | + | |
| 184 | + # SPX (index) vs SPY — the non-synchronous-session case | |
| 185 | + spx_bars = client.get_bars("index", "SPX", "1min", None, START, END) | |
| 186 | + spx_grid = rth_day_grids(spx_bars) | |
| 187 | + spx_r = locf_grid_returns(spx_grid, day_list) | |
| 188 | + spx_xc = nan_xcorr(spx_r, spy_r, MAX_LAG) | |
| 189 | + | |
| 190 | + # staleness -> artifact monotonicity (Spearman) | |
| 191 | + pairs = [ | |
| 192 | + (m["staleness"], m["spy_leads_+1"]) | |
| 193 | + for m in per_ticker.values() | |
| 194 | + if m.get("spy_leads_+1") is not None and m["staleness"] is not None | |
| 195 | + ] | |
| 196 | + xs = np.array([p[0] for p in pairs]) | |
| 197 | + ys = np.array([p[1] for p in pairs]) | |
| 198 | + rx = np.argsort(np.argsort(xs)).astype(float) | |
| 199 | + ry = np.argsort(np.argsort(ys)).astype(float) | |
| 200 | + spearman = float(np.corrcoef(rx, ry)[0, 1]) if len(pairs) > 5 else float("nan") | |
| 201 | + | |
| 202 | + # aggregates by staleness tercile | |
| 203 | + stale_vals = sorted(m["staleness"] for m in per_ticker.values()) | |
| 204 | + t1, t2 = np.percentile(stale_vals, [33.3, 66.7]) | |
| 205 | + | |
| 206 | + def tercile(s: float) -> str: | |
| 207 | + return "fresh" if s <= t1 else "mid" if s <= t2 else "stale" | |
| 208 | + | |
| 209 | + agg: dict[str, dict] = {} | |
| 210 | + for name in ("fresh", "mid", "stale"): | |
| 211 | + rows = [m for m in per_ticker.values() if tercile(m["staleness"]) == name] | |
| 212 | + if not rows: | |
| 213 | + continue | |
| 214 | + agg[name] = { | |
| 215 | + "n": len(rows), | |
| 216 | + "median_staleness": round(float(np.median([m["staleness"] for m in rows])), 4), | |
| 217 | + "median_ac1": round(float(np.median([m["ac1"] for m in rows])), 5), | |
| 218 | + "median_roll_spread": round( | |
| 219 | + float(np.median([m["roll_rel_spread"] for m in rows if m["roll_rel_spread"]])), 6 | |
| 220 | + ), | |
| 221 | + "median_vr5": round(float(np.median([m["vr5"] for m in rows])), 4), | |
| 222 | + "median_vr30": round(float(np.median([m["vr30"] for m in rows])), 4), | |
| 223 | + "median_spy_leads_+1": round( | |
| 224 | + float( | |
| 225 | + np.median( | |
| 226 | + [m["spy_leads_+1"] for m in rows if m.get("spy_leads_+1") is not None] | |
| 227 | + ) | |
| 228 | + ), | |
| 229 | + 5, | |
| 230 | + ), | |
| 231 | + } | |
| 232 | + | |
| 233 | + results = { | |
| 234 | + "experiment": "expB_artifact_baselines", | |
| 235 | + "run_utc": run_utc.isoformat(), | |
| 236 | + "author": "Simon-Pierre Boucher", | |
| 237 | + "contact": "contact@spboucher.ai", | |
| 238 | + "data_source": "hfmarketdata.io", | |
| 239 | + "protocol": { | |
| 240 | + "window": [START, END], | |
| 241 | + "adjustment": ADJ, | |
| 242 | + "rth": "09:30-16:00", | |
| 243 | + "liquid": LIQUID_STOCK + LIQUID_ETF, | |
| 244 | + "random_universe": list(random_universe), | |
| 245 | + "random_seed": RANDOM_SEED, | |
| 246 | + "boot": [BOOT_N, BOOT_SEED], | |
| 247 | + "confidence_level": 0, | |
| 248 | + "note": "artifact NULL levels — descriptive, in-sample by design", | |
| 249 | + }, | |
| 250 | + "per_ticker": per_ticker, | |
| 251 | + "terciles": {"cuts": [round(float(t1), 4), round(float(t2), 4)], "agg": agg}, | |
| 252 | + "staleness_vs_spy_lead_spearman": round(spearman, 4), | |
| 253 | + "spx_vs_spy_xcorr": { | |
| 254 | + str(k): round(v, 5) if np.isfinite(v) else None for k, v in spx_xc.items() | |
| 255 | + }, | |
| 256 | + "client_stats": vars(client.stats) | {"refreshes": list(client.stats.refreshes)}, | |
| 257 | + "manifest": collect_manifest(), | |
| 258 | + } | |
| 259 | + | |
| 260 | + out_dir = REPO_ROOT / "results" / "expB_artifact_baselines" / run_utc.strftime("%Y%m%dT%H%M%SZ") | |
| 261 | + out_dir.mkdir(parents=True) | |
| 262 | + (out_dir / "results.json").write_text(json.dumps(results, indent=2) + "\n") | |
| 263 | + print(f"\nwrote {out_dir.relative_to(REPO_ROOT)}/results.json") | |
| 264 | + print("terciles:", json.dumps(agg, indent=1)) | |
| 265 | + print("spearman(staleness, SPY leads +1):", round(spearman, 4)) | |
| 266 | + print("SPX vs SPY xcorr:", results["spx_vs_spy_xcorr"]) | |
| 32 | 267 | |
| 33 | 268 | |
| 34 | 269 | if __name__ == "__main__": |
modified
experiments/micro/expB_artifact_baselines/hypothesis.md
+52 −12
@@ -5,34 +5,74 @@ author: Simon-Pierre Boucher | ||
| 5 | 5 | contact: contact@spboucher.ai |
| 6 | 6 | data_source: hfmarketdata.io |
| 7 | 7 | created: 2026-08-12 |
| 8 | −status: draft | |
| 8 | +modified: 2026-08-12 | |
| 9 | +status: final | |
| 9 | 10 | --- |
| 10 | 11 | |
| 11 | 12 | # Hypothesis — expB_artifact_baselines |
| 12 | 13 | |
| 14 | +*Pre-specified 2026-08-12, before any real-data measurement. Detectors passed | |
| 15 | +the §8.1 synthetic gate first (11 tests, benchmarks/synthetic/).* | |
| 16 | + | |
| 13 | 17 | ```text |
| 14 | 18 | Hypothesis |
| 15 | − <what we believe and why — pre-specified BEFORE looking at results> | |
| 19 | + The three mechanical artifacts identified in expA are MEASURABLE and | |
| 20 | + MATERIAL in this dataset at 1-minute resolution: | |
| 21 | + (i) bid-ask bounce produces negative AC1 in 1min returns, larger for | |
| 22 | + less liquid names (Roll-implied relative spread as the null level); | |
| 23 | + (ii) illiquid names are stale on a large fraction of RTH minutes; | |
| 24 | + (iii) LOCF-gridding makes SPY spuriously "lead" stale tickers at +1 min, | |
| 25 | + with the artifact magnitude increasing in staleness. | |
| 16 | 26 | |
| 17 | 27 | Falsification criterion |
| 18 | − <the concrete measurable outcome that would prove this wrong> | |
| 28 | + The experiment fails if the nulls are unusable as baselines: bounce AC1 | |
| 29 | + indistinguishable from 0 across the liquidity spectrum (|median AC1| CI | |
| 30 | + covering 0 for the bottom-liquidity tercile), or no monotone relation | |
| 31 | + between staleness and the +1min SPY cross-correlation (Spearman rho <= 0 | |
| 32 | + across tickers). | |
| 19 | 33 | |
| 20 | 34 | Artifact null(s) |
| 21 | − <the fake-signal baseline(s) this must beat: bounce / staleness / | |
| 22 | − non-synchronous timestamps / permuted calendar / random walk> | |
| 35 | + This experiment BUILDS the artifact nulls; its own null is the synthetic | |
| 36 | + ground truth (§8.1 gate) — detectors verified to read 0 on random walks. | |
| 23 | 37 | |
| 24 | 38 | Method |
| 25 | − <exact procedure, universe, split (train/validation/holdout), seeds, | |
| 26 | − number of hypotheses tested, correction applied> | |
| 39 | + Universe (pre-specified): LIQUID = {AAPL MSFT NVDA AMZN GOOGL META TSLA | |
| 40 | + JPM XOM UNH} + {SPY QQQ}; RANDOM = 30 tickers drawn from the full 1min | |
| 41 | + stock ticker list with numpy seed 42 (deterministic given the cached | |
| 42 | + list). Window: 2024-01-02 → 2024-04-01, RTH only (09:30 ≤ t < 16:00), | |
| 43 | + adjustment adj_split, all data via hf_client (cached). | |
| 44 | + Per ticker: staleness ratio on the 390-min RTH grid; trade-to-trade 1min | |
| 45 | + log-return AC1 with moving-block bootstrap CI (block = 1 day, n=300, | |
| 46 | + seed=42); Roll implied relative spread; VR(5), VR(30). | |
| 47 | + Cross: lagged xcorr (±3 min, NaN-aware, day-boundary safe) of LOCF-grid | |
| 48 | + returns vs SPY; SPX-vs-SPY as the index-staleness case. | |
| 49 | + This is a DESCRIPTIVE measurement of artifact levels, not an anomaly | |
| 50 | + claim: no OOS split; every number is Level 0 by construction. | |
| 27 | 51 | |
| 28 | 52 | Result |
| 29 | − <filled after the run: effect size, bootstrap CIs, corrected p-values, | |
| 30 | − OOS status, cost-adjusted effect, credits used> | |
| 53 | + Run 20260812T055602Z (47 requests, 795 185 rows, 0 retries). 31/42 | |
| 54 | + tickers had enough data (>=30 days, >=2000 returns); 11 dropped, listed | |
| 55 | + in results.json. Median by staleness tercile (staleness | AC1 | Roll rel | |
| 56 | + spread | VR30 | SPY-leads-+1min): | |
| 57 | + fresh 0.000 | -0.009 | 1.4 bp | 0.972 | +0.005 | |
| 58 | + mid 0.107 | -0.051 | 3.8 bp | 0.901 | +0.047 | |
| 59 | + stale 0.691 | -0.232 | 17.0 bp | 0.547 | +0.021 | |
| 60 | + Spearman(staleness, SPY-leads-+1) = +0.43 (> 0: monotone). Extreme case | |
| 61 | + RITM.B: staleness 0.90, AC1 -0.253, VR30 0.35. Mega-caps: AC1 CI covers | |
| 62 | + 0 and the Roll estimator is undefined (positive autocov) — no measurable | |
| 63 | + bounce at the top. SPX-vs-SPY: corr 0.965 at lag 0 and +0.065 with SPY | |
| 64 | + leading by 1 min (index prints lag the tradable ETF). | |
| 31 | 65 | |
| 32 | 66 | Interpretation |
| 33 | − <what the numbers mean, WITH confidence level (0-3); alternative | |
| 34 | − explanations considered — artifact first> | |
| 67 | + Both falsification criteria FAILED to trigger: the nulls are usable. | |
| 68 | + Headline: an uncorrected VR/AC1 scan on mid/low-liquidity names is | |
| 69 | + DOMINATED by artifacts — VR30 of 0.55 and AC1 of -0.23 arise with no | |
| 70 | + economic mean reversion whatsoever. All numbers Level 0 (descriptive | |
| 71 | + null levels), as pre-specified. Magnitudes recorded in | |
| 72 | + research/artifact_taxonomy.md. | |
| 35 | 73 | |
| 36 | 74 | Next experiment |
| 37 | − <the most informative follow-up given this result> | |
| 75 | + expC (reversion scan) consumes these nulls: any reversion claim must | |
| 76 | + exceed the bounce null for its liquidity bucket. Phase 1 literature | |
| 77 | + sweep proceeds in parallel. | |
| 38 | 78 | ``` |
modified
research/LOG.md
+28 −0
@@ -54,3 +54,31 @@ the expB nulls. | ||
| 54 | 54 | |
| 55 | 55 | **Decision.** Full findings in research/data_source_profile.md (status: |
| 56 | 56 | reviewed). Next: expB_artifact_baselines + Phase 1 literature sweep. |
| 57 | + | |
| 58 | +## 2026-08-12 02:15 ET — expB complete: artifact nulls measured (after the §8.1 gate caught a real bug) | |
| 59 | + | |
| 60 | +**Question.** Are the bounce/staleness/non-synchronicity artifacts measurable | |
| 61 | +and material at 1min on this data? | |
| 62 | + | |
| 63 | +**Experiment.** Built generators with known ground truth + first real stats | |
| 64 | +modules (reversion, leadlag, bootstrap, artifacts). The mandatory synthetic | |
| 65 | +gate (11 tests) CAUGHT A REAL BUG before any real data was touched: the | |
| 66 | +variance-ratio estimator divided by q twice (VR ≈ 1/q on a pure random walk). | |
| 67 | +Fixed; all detectors then read 0 on random walks and recover planted effects. | |
| 68 | +expB then ran the pre-specified protocol (hypothesis.md written first): | |
| 69 | +12 liquid + 30 seed-42 random tickers, Q1 2024, RTH 1min, 47 requests, | |
| 70 | +795k rows. | |
| 71 | + | |
| 72 | +**Result.** Median by staleness tercile (staleness | AC1 | Roll spread | | |
| 73 | +VR30 | SPY-leads-+1min): fresh 0.00 | −0.009 | 1.4bp | 0.97 | +0.005; | |
| 74 | +mid 0.11 | −0.051 | 3.8bp | 0.90 | +0.047; stale 0.69 | −0.232 | 17bp | | |
| 75 | +0.55 | +0.021. Spearman(staleness, SPY-lead) = +0.43. SPX-vs-SPY: SPY leads | |
| 76 | +by 1min at +0.065. Mega-caps: no measurable bounce (AC1 CI covers 0). | |
| 77 | + | |
| 78 | +**Interpretation.** Uncorrected VR/AC1 scans on mid/low-liquidity names are | |
| 79 | +dominated by plumbing, not economics. Nulls usable; both falsification | |
| 80 | +criteria failed to trigger. All Level 0 by construction. | |
| 81 | + | |
| 82 | +**Decision.** artifact_taxonomy.md now carries 7 entries with measured | |
| 83 | +magnitudes (T1–T7). Next: expC consumes the bucket-matched bounce null; | |
| 84 | +Phase 1 literature sweep in parallel. | |
modified
research/artifact_taxonomy.md
+93 −5
@@ -5,13 +5,101 @@ author: Simon-Pierre Boucher | ||
| 5 | 5 | contact: contact@spboucher.ai |
| 6 | 6 | data_source: hfmarketdata.io |
| 7 | 7 | created: 2026-08-12 |
| 8 | −status: draft | |
| 8 | +modified: 2026-08-12 | |
| 9 | +status: reviewed | |
| 9 | 10 | --- |
| 10 | 11 | |
| 11 | 12 | # Artifact taxonomy (living document) |
| 12 | 13 | |
| 13 | −*Primary deliverable (Q4). Not yet written.* | |
| 14 | +The catalogue of mechanisms **in this specific dataset** that manufacture | |
| 15 | +fake anomalies — a primary deliverable (Q4). Every entry: mechanism, | |
| 16 | +detection, measured magnitude on this data, neutralization. Magnitudes from | |
| 17 | +`results/expA_data_reality/20260812T054515Z/` and | |
| 18 | +`results/expB_artifact_baselines/20260812T055602Z/` (Q1 2024, RTH 1min, | |
| 19 | +pre-specified universe). Detectors validated on synthetic ground truth first | |
| 20 | +(`benchmarks/synthetic/test_synthetic_gate.py`, 11 tests). | |
| 14 | 21 | |
| 15 | −Catalogue of artifacts in THIS dataset that masquerade as anomalies — | |
| 16 | −bid-ask bounce, stale prices, non-synchronous timestamps, survivorship, | |
| 17 | −look-ahead, corporate-action gaps — with detection and neutralization for each. | |
| 22 | +## T1 — Bid-ask bounce (Roll) | |
| 23 | + | |
| 24 | +* **Mechanism.** Trades alternate bid/ask; observed returns gain a negative | |
| 25 | + lag-1 autocovariance (−s²/4) with no economics. Masquerades as | |
| 26 | + mean-reversion (Q1). | |
| 27 | +* **Detection.** Roll implied relative spread `2·√(−autocov1)`; | |
| 28 | + bounce-implied AC1; `excess_reversion()` (validation/artifacts.py). | |
| 29 | +* **Measured.** Median AC1 by staleness tercile: −0.009 (fresh) / −0.051 | |
| 30 | + (mid) / **−0.232 (stale)**; Roll spread 1.4 / 3.8 / 17.0 bp. Mega-caps: | |
| 31 | + no measurable bounce (AC1 CI covers 0; Roll undefined ~half the time — | |
| 32 | + positive autocov). | |
| 33 | +* **Neutralize.** Report reversion net of the liquidity-bucket bounce null; | |
| 34 | + never average AC1 across liquidity buckets; treat Roll-undefined as | |
| 35 | + "no bounce measurable", not zero spread. | |
| 36 | + | |
| 37 | +## T2 — Stale prices / missing minutes | |
| 38 | + | |
| 39 | +* **Mechanism.** Bars exist only where trades occurred (expA: zero | |
| 40 | + zero-volume bars; an illiquid name printed 38 bars/day). LOCF joins add a | |
| 41 | + large mass of zero returns and depress variance-ratio statistics. | |
| 42 | +* **Detection.** `staleness_ratio` on the 390-min RTH grid. | |
| 43 | +* **Measured.** Staleness up to 0.90 (RITM.B); **VR(30) = 0.55 median for | |
| 44 | + the stale tercile — 0.35 extreme — with zero planted economics.** 11/42 | |
| 45 | + pre-specified tickers had too little data to analyze at all. | |
| 46 | +* **Neutralize.** Explicit grids with observed-masks (never silent LOCF); | |
| 47 | + liquidity filters pre-specified; VR/AC1 claims benchmarked against the | |
| 48 | + staleness-matched null, not against 1.0. | |
| 49 | + | |
| 50 | +## T3 — Non-synchronous lead-lag (LOCF cross-correlation) | |
| 51 | + | |
| 52 | +* **Mechanism.** A fresh series LOCF-joined to a stale one appears to LEAD | |
| 53 | + it: the stale print reflects old common information (classic | |
| 54 | + non-synchronous trading bias). | |
| 55 | +* **Detection.** Lagged cross-correlation vs SPY on the LOCF grid; synthetic | |
| 56 | + ground truth: rho=0.7 pair with 30 % observation → spurious +1 lag corr. | |
| 57 | +* **Measured.** SPY "leads" mid-staleness tickers by +0.047 at +1 min | |
| 58 | + (Spearman vs staleness +0.43). **SPX-vs-SPY: +0.065 with SPY leading | |
| 59 | + 1 min** at 0.965 contemporaneous corr — index prints lag the ETF. | |
| 60 | +* **Neutralize.** Any Q2 lead-lag claim must exceed the staleness-predicted | |
| 61 | + cross-correlation; test on synchronized (both-fresh) subsamples; index | |
| 62 | + series are stale by construction. | |
| 63 | + | |
| 64 | +## T4 — Auction close vs last bar | |
| 65 | + | |
| 66 | +* **Mechanism.** Daily bars carry the official closing-auction print; 1min | |
| 67 | + bars do not (expA: AAPL 312.41 daily close vs 312.49 last RTH 1min close). | |
| 68 | + Mixing conventions manufactures phantom overnight/close-to-close returns. | |
| 69 | +* **Measured.** 8 bp discrepancy on a calm day for the most liquid stock. | |
| 70 | +* **Neutralize.** Pick ONE close convention per experiment and state it; | |
| 71 | + never compute overnight returns across mixed conventions. | |
| 72 | + | |
| 73 | +## T5 — Rolling adjustment anchor | |
| 74 | + | |
| 75 | +* **Mechanism.** `adj_splitdiv` re-bases the whole history to the dataset | |
| 76 | + build date (expA: AAPL 2020-08-31 close = 125.17 adjusted vs 129.04 | |
| 77 | + traded). Adjusted series are not point-in-time stable → silent look-ahead | |
| 78 | + and irreproducibility if the cache is refreshed mid-study. | |
| 79 | +* **Neutralize.** Frozen local cache (hf_client never silently refetches); | |
| 80 | + data-manifest hash in every provenance; intraday work uses within-day | |
| 81 | + returns (adjustment-invariant) or UNADJUSTED plus explicit factors. | |
| 82 | + | |
| 83 | +## T6 — Daily vs intraday volume conventions | |
| 84 | + | |
| 85 | +* **Mechanism.** Daily volume includes auction/consolidated prints absent | |
| 86 | + from 1min bars (expA: 46.1 M daily vs 34.7 M extended-1min sum vs 25.7 M | |
| 87 | + RTH-1min sum for AAPL on one day — a 1.8× spread across conventions). | |
| 88 | +* **Neutralize.** Volume-based signals pick one convention; never mix daily | |
| 89 | + and intraday volume in one feature. | |
| 90 | + | |
| 91 | +## T7 — Vendor session / timezone semantics | |
| 92 | + | |
| 93 | +* **Mechanism.** All timestamps are US/Eastern wall-clock without a marker; | |
| 94 | + sessions differ per class (equities 04:00–19:59, SPX prints to 16:20, | |
| 95 | + futures ≈24 h, fx ET-week, crypto 24/7). Cross-asset joins on naive | |
| 96 | + timestamps silently compare different market states. | |
| 97 | +* **Neutralize.** One canonical calendar module (`data/calendars.py`), | |
| 98 | + explicit session filters per asset class, DST-aware conversions. | |
| 99 | + | |
| 100 | +--- | |
| 101 | + | |
| 102 | +*Open items: intraday-seasonality of spread/staleness (U-shape) as a | |
| 103 | +confounder for Q3 calendar scans (to be measured in expE); continuous-futures | |
| 104 | +splice choice (3 variants exposed by the API) as a testable artifact for | |
| 105 | +futures-based hypotheses.* | |
added
results/expB_artifact_baselines/20260812T055602Z/results.json
+963 −0
@@ -0,0 +1,963 @@ | ||
| 1 | +{ | |
| 2 | + "experiment": "expB_artifact_baselines", | |
| 3 | + "run_utc": "2026-08-12T05:56:02.079317+00:00", | |
| 4 | + "author": "Simon-Pierre Boucher", | |
| 5 | + "contact": "contact@spboucher.ai", | |
| 6 | + "data_source": "hfmarketdata.io", | |
| 7 | + "protocol": { | |
| 8 | + "window": [ | |
| 9 | + "2024-01-02", | |
| 10 | + "2024-04-01" | |
| 11 | + ], | |
| 12 | + "adjustment": "adj_split", | |
| 13 | + "rth": "09:30-16:00", | |
| 14 | + "liquid": [ | |
| 15 | + "AAPL", | |
| 16 | + "MSFT", | |
| 17 | + "NVDA", | |
| 18 | + "AMZN", | |
| 19 | + "GOOGL", | |
| 20 | + "META", | |
| 21 | + "TSLA", | |
| 22 | + "JPM", | |
| 23 | + "XOM", | |
| 24 | + "UNH", | |
| 25 | + "SPY", | |
| 26 | + "QQQ" | |
| 27 | + ], | |
| 28 | + "random_universe": [ | |
| 29 | + "ATRO", | |
| 30 | + "AUVI", | |
| 31 | + "AXDX", | |
| 32 | + "BKE", | |
| 33 | + "CECO", | |
| 34 | + "CKX", | |
| 35 | + "GCL", | |
| 36 | + "GRO", | |
| 37 | + "HPE", | |
| 38 | + "HTD", | |
| 39 | + "HWM", | |
| 40 | + "ICUI", | |
| 41 | + "KEY.K", | |
| 42 | + "KSPI", | |
| 43 | + "LENZ", | |
| 44 | + "LTSL", | |
| 45 | + "NSTS", | |
| 46 | + "NWAX", | |
| 47 | + "PBYI", | |
| 48 | + "PLNT", | |
| 49 | + "PSA.G", | |
| 50 | + "RDZN", | |
| 51 | + "RITM.B", | |
| 52 | + "RPM", | |
| 53 | + "RUM", | |
| 54 | + "SLF", | |
| 55 | + "SPB", | |
| 56 | + "STRRP", | |
| 57 | + "USGOW", | |
| 58 | + "WTFCM" | |
| 59 | + ], | |
| 60 | + "random_seed": 42, | |
| 61 | + "boot": [ | |
| 62 | + 300, | |
| 63 | + 42 | |
| 64 | + ], | |
| 65 | + "confidence_level": 0, | |
| 66 | + "note": "artifact NULL levels \u2014 descriptive, in-sample by design" | |
| 67 | + }, | |
| 68 | + "per_ticker": { | |
| 69 | + "AAPL": { | |
| 70 | + "days_covered": 61, | |
| 71 | + "staleness": 0.0, | |
| 72 | + "rth_fill_ratio": 1.0, | |
| 73 | + "n_trade_returns": 23729, | |
| 74 | + "ac1": 0.00162, | |
| 75 | + "ac1_ci95": [ | |
| 76 | + -0.01747, | |
| 77 | + 0.02147 | |
| 78 | + ], | |
| 79 | + "roll_rel_spread": null, | |
| 80 | + "vr5": 1.011, | |
| 81 | + "vr30": 1.0477, | |
| 82 | + "bucket": "liquid", | |
| 83 | + "asset": "stock", | |
| 84 | + "xcorr_vs_spy": { | |
| 85 | + "-3": 0.00528, | |
| 86 | + "-2": -0.0079, | |
| 87 | + "-1": 0.00678, | |
| 88 | + "0": 0.53789, | |
| 89 | + "1": 0.01237, | |
| 90 | + "2": -0.00536, | |
| 91 | + "3": -0.00986 | |
| 92 | + }, | |
| 93 | + "spy_leads_+1": 0.01237 | |
| 94 | + }, | |
| 95 | + "MSFT": { | |
| 96 | + "days_covered": 61, | |
| 97 | + "staleness": 0.0, | |
| 98 | + "rth_fill_ratio": 1.0, | |
| 99 | + "n_trade_returns": 23729, | |
| 100 | + "ac1": -0.01245, | |
| 101 | + "ac1_ci95": [ | |
| 102 | + -0.04532, | |
| 103 | + 0.01955 | |
| 104 | + ], | |
| 105 | + "roll_rel_spread": 0.000111, | |
| 106 | + "vr5": 0.9523, | |
| 107 | + "vr30": 0.9179, | |
| 108 | + "bucket": "liquid", | |
| 109 | + "asset": "stock", | |
| 110 | + "xcorr_vs_spy": { | |
| 111 | + "-3": -0.0054, | |
| 112 | + "-2": 0.00084, | |
| 113 | + "-1": 0.00872, | |
| 114 | + "0": 0.59532, | |
| 115 | + "1": 0.00645, | |
| 116 | + "2": -0.00534, | |
| 117 | + "3": -0.00479 | |
| 118 | + }, | |
| 119 | + "spy_leads_+1": 0.00645 | |
| 120 | + }, | |
| 121 | + "NVDA": { | |
| 122 | + "days_covered": 61, | |
| 123 | + "staleness": 0.0, | |
| 124 | + "rth_fill_ratio": 1.0, | |
| 125 | + "n_trade_returns": 23729, | |
| 126 | + "ac1": 0.01298, | |
| 127 | + "ac1_ci95": [ | |
| 128 | + -0.01676, | |
| 129 | + 0.04068 | |
| 130 | + ], | |
| 131 | + "roll_rel_spread": null, | |
| 132 | + "vr5": 0.9979, | |
| 133 | + "vr30": 0.9733, | |
| 134 | + "bucket": "liquid", | |
| 135 | + "asset": "stock", | |
| 136 | + "xcorr_vs_spy": { | |
| 137 | + "-3": 0.00212, | |
| 138 | + "-2": 0.01372, | |
| 139 | + "-1": 0.02509, | |
| 140 | + "0": 0.53888, | |
| 141 | + "1": 0.00859, | |
| 142 | + "2": -0.01068, | |
| 143 | + "3": -0.01252 | |
| 144 | + }, | |
| 145 | + "spy_leads_+1": 0.00859 | |
| 146 | + }, | |
| 147 | + "AMZN": { | |
| 148 | + "days_covered": 61, | |
| 149 | + "staleness": 0.0, | |
| 150 | + "rth_fill_ratio": 1.0, | |
| 151 | + "n_trade_returns": 23729, | |
| 152 | + "ac1": -0.00916, | |
| 153 | + "ac1_ci95": [ | |
| 154 | + -0.03589, | |
| 155 | + 0.01421 | |
| 156 | + ], | |
| 157 | + "roll_rel_spread": 0.000117, | |
| 158 | + "vr5": 0.946, | |
| 159 | + "vr30": 0.8658, | |
| 160 | + "bucket": "liquid", | |
| 161 | + "asset": "stock", | |
| 162 | + "xcorr_vs_spy": { | |
| 163 | + "-3": -0.01025, | |
| 164 | + "-2": -0.00672, | |
| 165 | + "-1": 0.00274, | |
| 166 | + "0": 0.5434, | |
| 167 | + "1": 0.0037, | |
| 168 | + "2": -0.00722, | |
| 169 | + "3": 0.01063 | |
| 170 | + }, | |
| 171 | + "spy_leads_+1": 0.0037 | |
| 172 | + }, | |
| 173 | + "GOOGL": { | |
| 174 | + "days_covered": 61, | |
| 175 | + "staleness": 0.0, | |
| 176 | + "rth_fill_ratio": 1.0, | |
| 177 | + "n_trade_returns": 23729, | |
| 178 | + "ac1": -0.02624, | |
| 179 | + "ac1_ci95": [ | |
| 180 | + -0.06462, | |
| 181 | + 0.01378 | |
| 182 | + ], | |
| 183 | + "roll_rel_spread": 0.000196, | |
| 184 | + "vr5": 0.9769, | |
| 185 | + "vr30": 0.9116, | |
| 186 | + "bucket": "liquid", | |
| 187 | + "asset": "stock", | |
| 188 | + "xcorr_vs_spy": { | |
| 189 | + "-3": 0.00045, | |
| 190 | + "-2": -0.00117, | |
| 191 | + "-1": -0.00146, | |
| 192 | + "0": 0.4932, | |
| 193 | + "1": -0.00674, | |
| 194 | + "2": -0.00411, | |
| 195 | + "3": 0.00345 | |
| 196 | + }, | |
| 197 | + "spy_leads_+1": -0.00674 | |
| 198 | + }, | |
| 199 | + "META": { | |
| 200 | + "days_covered": 61, | |
| 201 | + "staleness": 0.0, | |
| 202 | + "rth_fill_ratio": 1.0, | |
| 203 | + "n_trade_returns": 23729, | |
| 204 | + "ac1": -0.02418, | |
| 205 | + "ac1_ci95": [ | |
| 206 | + -0.05678, | |
| 207 | + 0.0063 | |
| 208 | + ], | |
| 209 | + "roll_rel_spread": 0.000244, | |
| 210 | + "vr5": 0.9732, | |
| 211 | + "vr30": 0.9473, | |
| 212 | + "bucket": "liquid", | |
| 213 | + "asset": "stock", | |
| 214 | + "xcorr_vs_spy": { | |
| 215 | + "-3": 0.00716, | |
| 216 | + "-2": 0.00212, | |
| 217 | + "-1": 0.01956, | |
| 218 | + "0": 0.48589, | |
| 219 | + "1": 0.00367, | |
| 220 | + "2": -0.00819, | |
| 221 | + "3": 0.00193 | |
| 222 | + }, | |
| 223 | + "spy_leads_+1": 0.00367 | |
| 224 | + }, | |
| 225 | + "TSLA": { | |
| 226 | + "days_covered": 61, | |
| 227 | + "staleness": 0.0, | |
| 228 | + "rth_fill_ratio": 1.0, | |
| 229 | + "n_trade_returns": 23729, | |
| 230 | + "ac1": 0.01645, | |
| 231 | + "ac1_ci95": [ | |
| 232 | + -0.00215, | |
| 233 | + 0.03578 | |
| 234 | + ], | |
| 235 | + "roll_rel_spread": null, | |
| 236 | + "vr5": 1.0053, | |
| 237 | + "vr30": 1.0155, | |
| 238 | + "bucket": "liquid", | |
| 239 | + "asset": "stock", | |
| 240 | + "xcorr_vs_spy": { | |
| 241 | + "-3": -0.00901, | |
| 242 | + "-2": -0.00031, | |
| 243 | + "-1": 0.01181, | |
| 244 | + "0": 0.40916, | |
| 245 | + "1": 0.00585, | |
| 246 | + "2": -0.00239, | |
| 247 | + "3": 0.00135 | |
| 248 | + }, | |
| 249 | + "spy_leads_+1": 0.00585 | |
| 250 | + }, | |
| 251 | + "JPM": { | |
| 252 | + "days_covered": 61, | |
| 253 | + "staleness": 0.0, | |
| 254 | + "rth_fill_ratio": 1.0, | |
| 255 | + "n_trade_returns": 23729, | |
| 256 | + "ac1": -0.03379, | |
| 257 | + "ac1_ci95": [ | |
| 258 | + -0.05157, | |
| 259 | + -0.01069 | |
| 260 | + ], | |
| 261 | + "roll_rel_spread": 0.000162, | |
| 262 | + "vr5": 0.9574, | |
| 263 | + "vr30": 0.9499, | |
| 264 | + "bucket": "liquid", | |
| 265 | + "asset": "stock", | |
| 266 | + "xcorr_vs_spy": { | |
| 267 | + "-3": 0.00115, | |
| 268 | + "-2": -0.00947, | |
| 269 | + "-1": 0.00796, | |
| 270 | + "0": 0.4252, | |
| 271 | + "1": 0.00377, | |
| 272 | + "2": 0.01337, | |
| 273 | + "3": -0.00602 | |
| 274 | + }, | |
| 275 | + "spy_leads_+1": 0.00377 | |
| 276 | + }, | |
| 277 | + "XOM": { | |
| 278 | + "days_covered": 61, | |
| 279 | + "staleness": 0.0, | |
| 280 | + "rth_fill_ratio": 1.0, | |
| 281 | + "n_trade_returns": 23729, | |
| 282 | + "ac1": -0.01076, | |
| 283 | + "ac1_ci95": [ | |
| 284 | + -0.03129, | |
| 285 | + 0.00755 | |
| 286 | + ], | |
| 287 | + "roll_rel_spread": 0.000105, | |
| 288 | + "vr5": 0.9895, | |
| 289 | + "vr30": 1.0847, | |
| 290 | + "bucket": "liquid", | |
| 291 | + "asset": "stock", | |
| 292 | + "xcorr_vs_spy": { | |
| 293 | + "-3": 0.00277, | |
| 294 | + "-2": -0.01127, | |
| 295 | + "-1": 0.00023, | |
| 296 | + "0": 0.20757, | |
| 297 | + "1": -0.00728, | |
| 298 | + "2": 0.00613, | |
| 299 | + "3": 0.00189 | |
| 300 | + }, | |
| 301 | + "spy_leads_+1": -0.00728 | |
| 302 | + }, | |
| 303 | + "UNH": { | |
| 304 | + "days_covered": 61, | |
| 305 | + "staleness": 0.0024, | |
| 306 | + "rth_fill_ratio": 0.9976, | |
| 307 | + "n_trade_returns": 23673, | |
| 308 | + "ac1": -0.05529, | |
| 309 | + "ac1_ci95": [ | |
| 310 | + -0.08843, | |
| 311 | + -0.02247 | |
| 312 | + ], | |
| 313 | + "roll_rel_spread": 0.000279, | |
| 314 | + "vr5": 0.8895, | |
| 315 | + "vr30": 0.8411, | |
| 316 | + "bucket": "liquid", | |
| 317 | + "asset": "stock", | |
| 318 | + "xcorr_vs_spy": { | |
| 319 | + "-3": -0.00305, | |
| 320 | + "-2": -0.01299, | |
| 321 | + "-1": 0.00162, | |
| 322 | + "0": 0.12185, | |
| 323 | + "1": 0.02334, | |
| 324 | + "2": -4e-05, | |
| 325 | + "3": 0.00096 | |
| 326 | + }, | |
| 327 | + "spy_leads_+1": 0.02334 | |
| 328 | + }, | |
| 329 | + "SPY": { | |
| 330 | + "days_covered": 61, | |
| 331 | + "staleness": 0.0, | |
| 332 | + "rth_fill_ratio": 1.0, | |
| 333 | + "n_trade_returns": 23729, | |
| 334 | + "ac1": 0.00628, | |
| 335 | + "ac1_ci95": [ | |
| 336 | + -0.01811, | |
| 337 | + 0.02379 | |
| 338 | + ], | |
| 339 | + "roll_rel_spread": null, | |
| 340 | + "vr5": 1.0101, | |
| 341 | + "vr30": 0.9718, | |
| 342 | + "bucket": "liquid", | |
| 343 | + "asset": "etf" | |
| 344 | + }, | |
| 345 | + "QQQ": { | |
| 346 | + "days_covered": 61, | |
| 347 | + "staleness": 0.0, | |
| 348 | + "rth_fill_ratio": 1.0, | |
| 349 | + "n_trade_returns": 23729, | |
| 350 | + "ac1": 0.01507, | |
| 351 | + "ac1_ci95": [ | |
| 352 | + -0.00676, | |
| 353 | + 0.0325 | |
| 354 | + ], | |
| 355 | + "roll_rel_spread": null, | |
| 356 | + "vr5": 1.0233, | |
| 357 | + "vr30": 1.0059, | |
| 358 | + "bucket": "liquid", | |
| 359 | + "asset": "etf", | |
| 360 | + "xcorr_vs_spy": { | |
| 361 | + "-3": 0.00069, | |
| 362 | + "-2": 0.00181, | |
| 363 | + "-1": 0.01376, | |
| 364 | + "0": 0.90007, | |
| 365 | + "1": 0.01068, | |
| 366 | + "2": -0.00847, | |
| 367 | + "3": -0.00557 | |
| 368 | + }, | |
| 369 | + "spy_leads_+1": 0.01068 | |
| 370 | + }, | |
| 371 | + "ATRO": { | |
| 372 | + "days_covered": 61, | |
| 373 | + "staleness": 0.6919, | |
| 374 | + "rth_fill_ratio": 0.3081, | |
| 375 | + "n_trade_returns": 7269, | |
| 376 | + "ac1": -0.05122, | |
| 377 | + "ac1_ci95": [ | |
| 378 | + -0.08802, | |
| 379 | + -0.01873 | |
| 380 | + ], | |
| 381 | + "roll_rel_spread": 0.001022, | |
| 382 | + "vr5": 0.8975, | |
| 383 | + "vr30": 0.9242, | |
| 384 | + "bucket": "random", | |
| 385 | + "asset": "stock", | |
| 386 | + "xcorr_vs_spy": { | |
| 387 | + "-3": 0.00995, | |
| 388 | + "-2": -0.00133, | |
| 389 | + "-1": -0.00238, | |
| 390 | + "0": 0.09732, | |
| 391 | + "1": 0.06282, | |
| 392 | + "2": 0.03343, | |
| 393 | + "3": 0.0201 | |
| 394 | + }, | |
| 395 | + "spy_leads_+1": 0.06282 | |
| 396 | + }, | |
| 397 | + "AUVI": { | |
| 398 | + "days_covered": 61, | |
| 399 | + "staleness": 0.6646, | |
| 400 | + "rth_fill_ratio": 0.3354, | |
| 401 | + "n_trade_returns": 7917, | |
| 402 | + "ac1": -0.21019, | |
| 403 | + "ac1_ci95": [ | |
| 404 | + -0.28933, | |
| 405 | + -0.11986 | |
| 406 | + ], | |
| 407 | + "roll_rel_spread": 0.008811, | |
| 408 | + "vr5": 0.6587, | |
| 409 | + "vr30": 0.6087, | |
| 410 | + "bucket": "random", | |
| 411 | + "asset": "stock", | |
| 412 | + "xcorr_vs_spy": { | |
| 413 | + "-3": 0.0033, | |
| 414 | + "-2": 0.00165, | |
| 415 | + "-1": -0.0025, | |
| 416 | + "0": 0.01784, | |
| 417 | + "1": 0.0057, | |
| 418 | + "2": 0.01093, | |
| 419 | + "3": -0.00067 | |
| 420 | + }, | |
| 421 | + "spy_leads_+1": 0.0057 | |
| 422 | + }, | |
| 423 | + "AXDX": { | |
| 424 | + "days_covered": 61, | |
| 425 | + "staleness": 0.8335, | |
| 426 | + "rth_fill_ratio": 0.1665, | |
| 427 | + "n_trade_returns": 3901, | |
| 428 | + "ac1": -0.32299, | |
| 429 | + "ac1_ci95": [ | |
| 430 | + -0.35528, | |
| 431 | + -0.27751 | |
| 432 | + ], | |
| 433 | + "roll_rel_spread": 0.019373, | |
| 434 | + "vr5": 0.4941, | |
| 435 | + "vr30": 0.3858, | |
| 436 | + "bucket": "random", | |
| 437 | + "asset": "stock", | |
| 438 | + "xcorr_vs_spy": { | |
| 439 | + "-3": 0.00371, | |
| 440 | + "-2": 0.01291, | |
| 441 | + "-1": -0.0068, | |
| 442 | + "0": 0.00065, | |
| 443 | + "1": -0.00701, | |
| 444 | + "2": 0.00379, | |
| 445 | + "3": 0.01124 | |
| 446 | + }, | |
| 447 | + "spy_leads_+1": -0.00701 | |
| 448 | + }, | |
| 449 | + "BKE": { | |
| 450 | + "days_covered": 61, | |
| 451 | + "staleness": 0.288, | |
| 452 | + "rth_fill_ratio": 0.712, | |
| 453 | + "n_trade_returns": 16878, | |
| 454 | + "ac1": -0.04574, | |
| 455 | + "ac1_ci95": [ | |
| 456 | + -0.07188, | |
| 457 | + -0.019 | |
| 458 | + ], | |
| 459 | + "roll_rel_spread": 0.000494, | |
| 460 | + "vr5": 0.9229, | |
| 461 | + "vr30": 0.8628, | |
| 462 | + "bucket": "random", | |
| 463 | + "asset": "stock", | |
| 464 | + "xcorr_vs_spy": { | |
| 465 | + "-3": -0.00949, | |
| 466 | + "-2": -0.00335, | |
| 467 | + "-1": -0.01292, | |
| 468 | + "0": 0.21079, | |
| 469 | + "1": 0.08981, | |
| 470 | + "2": 0.03184, | |
| 471 | + "3": -0.00685 | |
| 472 | + }, | |
| 473 | + "spy_leads_+1": 0.08981 | |
| 474 | + }, | |
| 475 | + "CECO": { | |
| 476 | + "days_covered": 61, | |
| 477 | + "staleness": 0.5311, | |
| 478 | + "rth_fill_ratio": 0.4689, | |
| 479 | + "n_trade_returns": 11093, | |
| 480 | + "ac1": -0.16489, | |
| 481 | + "ac1_ci95": [ | |
| 482 | + -0.23411, | |
| 483 | + -0.07077 | |
| 484 | + ], | |
| 485 | + "roll_rel_spread": 0.001807, | |
| 486 | + "vr5": 0.7867, | |
| 487 | + "vr30": 1.0073, | |
| 488 | + "bucket": "random", | |
| 489 | + "asset": "stock", | |
| 490 | + "xcorr_vs_spy": { | |
| 491 | + "-3": -0.01082, | |
| 492 | + "-2": 0.00211, | |
| 493 | + "-1": 0.00371, | |
| 494 | + "0": 0.09421, | |
| 495 | + "1": 0.06433, | |
| 496 | + "2": 0.03838, | |
| 497 | + "3": 0.01926 | |
| 498 | + }, | |
| 499 | + "spy_leads_+1": 0.06433 | |
| 500 | + }, | |
| 501 | + "HPE": { | |
| 502 | + "days_covered": 61, | |
| 503 | + "staleness": 0.0003, | |
| 504 | + "rth_fill_ratio": 0.9997, | |
| 505 | + "n_trade_returns": 23723, | |
| 506 | + "ac1": -0.06183, | |
| 507 | + "ac1_ci95": [ | |
| 508 | + -0.08556, | |
| 509 | + -0.03994 | |
| 510 | + ], | |
| 511 | + "roll_rel_spread": 0.000504, | |
| 512 | + "vr5": 0.8673, | |
| 513 | + "vr30": 1.0189, | |
| 514 | + "bucket": "random", | |
| 515 | + "asset": "stock", | |
| 516 | + "xcorr_vs_spy": { | |
| 517 | + "-3": 0.00082, | |
| 518 | + "-2": 0.00395, | |
| 519 | + "-1": -0.00307, | |
| 520 | + "0": 0.27357, | |
| 521 | + "1": 0.00666, | |
| 522 | + "2": -0.02272, | |
| 523 | + "3": -0.00514 | |
| 524 | + }, | |
| 525 | + "spy_leads_+1": 0.00666 | |
| 526 | + }, | |
| 527 | + "HTD": { | |
| 528 | + "days_covered": 61, | |
| 529 | + "staleness": 0.6906, | |
| 530 | + "rth_fill_ratio": 0.3094, | |
| 531 | + "n_trade_returns": 7300, | |
| 532 | + "ac1": -0.31279, | |
| 533 | + "ac1_ci95": [ | |
| 534 | + -0.35006, | |
| 535 | + -0.27744 | |
| 536 | + ], | |
| 537 | + "roll_rel_spread": 0.00128, | |
| 538 | + "vr5": 0.5425, | |
| 539 | + "vr30": 0.4852, | |
| 540 | + "bucket": "random", | |
| 541 | + "asset": "stock", | |
| 542 | + "xcorr_vs_spy": { | |
| 543 | + "-3": 0.00289, | |
| 544 | + "-2": 0.00058, | |
| 545 | + "-1": -0.00173, | |
| 546 | + "0": 0.03006, | |
| 547 | + "1": 0.05151, | |
| 548 | + "2": 0.04017, | |
| 549 | + "3": 0.01686 | |
| 550 | + }, | |
| 551 | + "spy_leads_+1": 0.05151 | |
| 552 | + }, | |
| 553 | + "HWM": { | |
| 554 | + "days_covered": 61, | |
| 555 | + "staleness": 0.0052, | |
| 556 | + "rth_fill_ratio": 0.9948, | |
| 557 | + "n_trade_returns": 23605, | |
| 558 | + "ac1": -0.02708, | |
| 559 | + "ac1_ci95": [ | |
| 560 | + -0.05571, | |
| 561 | + 0.00259 | |
| 562 | + ], | |
| 563 | + "roll_rel_spread": 0.00019, | |
| 564 | + "vr5": 0.9716, | |
| 565 | + "vr30": 0.9134, | |
| 566 | + "bucket": "random", | |
| 567 | + "asset": "stock", | |
| 568 | + "xcorr_vs_spy": { | |
| 569 | + "-3": 0.00501, | |
| 570 | + "-2": -0.01232, | |
| 571 | + "-1": 0.00072, | |
| 572 | + "0": 0.33034, | |
| 573 | + "1": 0.02045, | |
| 574 | + "2": 0.00126, | |
| 575 | + "3": -0.00136 | |
| 576 | + }, | |
| 577 | + "spy_leads_+1": 0.02045 | |
| 578 | + }, | |
| 579 | + "ICUI": { | |
| 580 | + "days_covered": 61, | |
| 581 | + "staleness": 0.4941, | |
| 582 | + "rth_fill_ratio": 0.5059, | |
| 583 | + "n_trade_returns": 11975, | |
| 584 | + "ac1": -0.10807, | |
| 585 | + "ac1_ci95": [ | |
| 586 | + -0.13625, | |
| 587 | + -0.07268 | |
| 588 | + ], | |
| 589 | + "roll_rel_spread": 0.001372, | |
| 590 | + "vr5": 0.8884, | |
| 591 | + "vr30": 0.8928, | |
| 592 | + "bucket": "random", | |
| 593 | + "asset": "stock", | |
| 594 | + "xcorr_vs_spy": { | |
| 595 | + "-3": 0.00216, | |
| 596 | + "-2": 0.01038, | |
| 597 | + "-1": -0.00739, | |
| 598 | + "0": 0.12354, | |
| 599 | + "1": 0.0769, | |
| 600 | + "2": 0.03793, | |
| 601 | + "3": -0.00457 | |
| 602 | + }, | |
| 603 | + "spy_leads_+1": 0.0769 | |
| 604 | + }, | |
| 605 | + "KEY.K": { | |
| 606 | + "days_covered": 61, | |
| 607 | + "staleness": 0.8674, | |
| 608 | + "rth_fill_ratio": 0.1326, | |
| 609 | + "n_trade_returns": 3093, | |
| 610 | + "ac1": -0.3469, | |
| 611 | + "ac1_ci95": [ | |
| 612 | + -0.38592, | |
| 613 | + -0.28806 | |
| 614 | + ], | |
| 615 | + "roll_rel_spread": 0.005038, | |
| 616 | + "vr5": 0.4251, | |
| 617 | + "vr30": 0.2543, | |
| 618 | + "bucket": "random", | |
| 619 | + "asset": "stock", | |
| 620 | + "xcorr_vs_spy": { | |
| 621 | + "-3": 0.01268, | |
| 622 | + "-2": 0.01152, | |
| 623 | + "-1": 0.00011, | |
| 624 | + "0": 0.00819, | |
| 625 | + "1": 0.02132, | |
| 626 | + "2": -0.00214, | |
| 627 | + "3": 0.00553 | |
| 628 | + }, | |
| 629 | + "spy_leads_+1": 0.02132 | |
| 630 | + }, | |
| 631 | + "KSPI": { | |
| 632 | + "days_covered": 48, | |
| 633 | + "staleness": 0.471, | |
| 634 | + "rth_fill_ratio": 0.529, | |
| 635 | + "n_trade_returns": 9855, | |
| 636 | + "ac1": -0.20949, | |
| 637 | + "ac1_ci95": [ | |
| 638 | + -0.25785, | |
| 639 | + -0.16733 | |
| 640 | + ], | |
| 641 | + "roll_rel_spread": 0.001597, | |
| 642 | + "vr5": 0.6778, | |
| 643 | + "vr30": 0.6525, | |
| 644 | + "bucket": "random", | |
| 645 | + "asset": "stock", | |
| 646 | + "xcorr_vs_spy": { | |
| 647 | + "-3": -0.00345, | |
| 648 | + "-2": -0.00331, | |
| 649 | + "-1": 0.01628, | |
| 650 | + "0": 0.01032, | |
| 651 | + "1": 0.02003, | |
| 652 | + "2": 0.00966, | |
| 653 | + "3": -0.00787 | |
| 654 | + }, | |
| 655 | + "spy_leads_+1": 0.02003 | |
| 656 | + }, | |
| 657 | + "LENZ": { | |
| 658 | + "days_covered": 61, | |
| 659 | + "staleness": 0.7067, | |
| 660 | + "rth_fill_ratio": 0.2933, | |
| 661 | + "n_trade_returns": 6916, | |
| 662 | + "ac1": -0.27902, | |
| 663 | + "ac1_ci95": [ | |
| 664 | + -0.35931, | |
| 665 | + -0.19937 | |
| 666 | + ], | |
| 667 | + "roll_rel_spread": 0.007069, | |
| 668 | + "vr5": 0.623, | |
| 669 | + "vr30": 0.464, | |
| 670 | + "bucket": "random", | |
| 671 | + "asset": "stock", | |
| 672 | + "xcorr_vs_spy": { | |
| 673 | + "-3": 0.01234, | |
| 674 | + "-2": 0.00659, | |
| 675 | + "-1": 0.00154, | |
| 676 | + "0": 0.00207, | |
| 677 | + "1": 0.0143, | |
| 678 | + "2": 0.00425, | |
| 679 | + "3": -0.00163 | |
| 680 | + }, | |
| 681 | + "spy_leads_+1": 0.0143 | |
| 682 | + }, | |
| 683 | + "PBYI": { | |
| 684 | + "days_covered": 61, | |
| 685 | + "staleness": 0.3251, | |
| 686 | + "rth_fill_ratio": 0.6749, | |
| 687 | + "n_trade_returns": 15996, | |
| 688 | + "ac1": -0.11171, | |
| 689 | + "ac1_ci95": [ | |
| 690 | + -0.14442, | |
| 691 | + -0.07365 | |
| 692 | + ], | |
| 693 | + "roll_rel_spread": 0.002591, | |
| 694 | + "vr5": 0.8282, | |
| 695 | + "vr30": 0.82, | |
| 696 | + "bucket": "random", | |
| 697 | + "asset": "stock", | |
| 698 | + "xcorr_vs_spy": { | |
| 699 | + "-3": -0.00863, | |
| 700 | + "-2": -0.00987, | |
| 701 | + "-1": 0.0077, | |
| 702 | + "0": 0.04659, | |
| 703 | + "1": 0.03636, | |
| 704 | + "2": 0.00891, | |
| 705 | + "3": 0.00204 | |
| 706 | + }, | |
| 707 | + "spy_leads_+1": 0.03636 | |
| 708 | + }, | |
| 709 | + "PLNT": { | |
| 710 | + "days_covered": 61, | |
| 711 | + "staleness": 0.0305, | |
| 712 | + "rth_fill_ratio": 0.9695, | |
| 713 | + "n_trade_returns": 23004, | |
| 714 | + "ac1": -0.01553, | |
| 715 | + "ac1_ci95": [ | |
| 716 | + -0.03743, | |
| 717 | + 0.00607 | |
| 718 | + ], | |
| 719 | + "roll_rel_spread": 0.000235, | |
| 720 | + "vr5": 0.9942, | |
| 721 | + "vr30": 1.0098, | |
| 722 | + "bucket": "random", | |
| 723 | + "asset": "stock", | |
| 724 | + "xcorr_vs_spy": { | |
| 725 | + "-3": -0.00276, | |
| 726 | + "-2": 0.01403, | |
| 727 | + "-1": 0.00527, | |
| 728 | + "0": 0.20839, | |
| 729 | + "1": 0.03961, | |
| 730 | + "2": -0.00434, | |
| 731 | + "3": -0.00844 | |
| 732 | + }, | |
| 733 | + "spy_leads_+1": 0.03961 | |
| 734 | + }, | |
| 735 | + "RITM.B": { | |
| 736 | + "days_covered": 61, | |
| 737 | + "staleness": 0.8984, | |
| 738 | + "rth_fill_ratio": 0.1016, | |
| 739 | + "n_trade_returns": 2356, | |
| 740 | + "ac1": -0.25345, | |
| 741 | + "ac1_ci95": [ | |
| 742 | + -0.30424, | |
| 743 | + -0.17296 | |
| 744 | + ], | |
| 745 | + "roll_rel_spread": 0.001261, | |
| 746 | + "vr5": 0.5839, | |
| 747 | + "vr30": 0.3502, | |
| 748 | + "bucket": "random", | |
| 749 | + "asset": "stock", | |
| 750 | + "xcorr_vs_spy": { | |
| 751 | + "-3": 0.00127, | |
| 752 | + "-2": 0.01134, | |
| 753 | + "-1": -0.01608, | |
| 754 | + "0": -0.00305, | |
| 755 | + "1": 0.01628, | |
| 756 | + "2": 0.00606, | |
| 757 | + "3": 0.00448 | |
| 758 | + }, | |
| 759 | + "spy_leads_+1": 0.01628 | |
| 760 | + }, | |
| 761 | + "RPM": { | |
| 762 | + "days_covered": 61, | |
| 763 | + "staleness": 0.2196, | |
| 764 | + "rth_fill_ratio": 0.7804, | |
| 765 | + "n_trade_returns": 18504, | |
| 766 | + "ac1": -0.0903, | |
| 767 | + "ac1_ci95": [ | |
| 768 | + -0.13912, | |
| 769 | + -0.06355 | |
| 770 | + ], | |
| 771 | + "roll_rel_spread": 0.000398, | |
| 772 | + "vr5": 0.8569, | |
| 773 | + "vr30": 0.8519, | |
| 774 | + "bucket": "random", | |
| 775 | + "asset": "stock", | |
| 776 | + "xcorr_vs_spy": { | |
| 777 | + "-3": 0.00269, | |
| 778 | + "-2": 0.01379, | |
| 779 | + "-1": -0.01292, | |
| 780 | + "0": 0.25954, | |
| 781 | + "1": 0.09142, | |
| 782 | + "2": 0.01975, | |
| 783 | + "3": 0.02268 | |
| 784 | + }, | |
| 785 | + "spy_leads_+1": 0.09142 | |
| 786 | + }, | |
| 787 | + "RUM": { | |
| 788 | + "days_covered": 61, | |
| 789 | + "staleness": 0.0291, | |
| 790 | + "rth_fill_ratio": 0.9709, | |
| 791 | + "n_trade_returns": 23037, | |
| 792 | + "ac1": -0.05627, | |
| 793 | + "ac1_ci95": [ | |
| 794 | + -0.09747, | |
| 795 | + -0.01213 | |
| 796 | + ], | |
| 797 | + "roll_rel_spread": 0.001713, | |
| 798 | + "vr5": 0.9298, | |
| 799 | + "vr30": 0.8893, | |
| 800 | + "bucket": "random", | |
| 801 | + "asset": "stock", | |
| 802 | + "xcorr_vs_spy": { | |
| 803 | + "-3": 0.01104, | |
| 804 | + "-2": -0.00292, | |
| 805 | + "-1": 0.00475, | |
| 806 | + "0": 0.13733, | |
| 807 | + "1": 0.05423, | |
| 808 | + "2": 0.0155, | |
| 809 | + "3": -0.00228 | |
| 810 | + }, | |
| 811 | + "spy_leads_+1": 0.05423 | |
| 812 | + }, | |
| 813 | + "SLF": { | |
| 814 | + "days_covered": 61, | |
| 815 | + "staleness": 0.1826, | |
| 816 | + "rth_fill_ratio": 0.8174, | |
| 817 | + "n_trade_returns": 19386, | |
| 818 | + "ac1": -0.01447, | |
| 819 | + "ac1_ci95": [ | |
| 820 | + -0.03021, | |
| 821 | + 0.01033 | |
| 822 | + ], | |
| 823 | + "roll_rel_spread": 0.000111, | |
| 824 | + "vr5": 0.997, | |
| 825 | + "vr30": 0.9691, | |
| 826 | + "bucket": "random", | |
| 827 | + "asset": "stock", | |
| 828 | + "xcorr_vs_spy": { | |
| 829 | + "-3": 0.00193, | |
| 830 | + "-2": 0.00374, | |
| 831 | + "-1": -0.00431, | |
| 832 | + "0": 0.32152, | |
| 833 | + "1": 0.11852, | |
| 834 | + "2": 0.01432, | |
| 835 | + "3": 0.01368 | |
| 836 | + }, | |
| 837 | + "spy_leads_+1": 0.11852 | |
| 838 | + }, | |
| 839 | + "SPB": { | |
| 840 | + "days_covered": 61, | |
| 841 | + "staleness": 0.3332, | |
| 842 | + "rth_fill_ratio": 0.6668, | |
| 843 | + "n_trade_returns": 15801, | |
| 844 | + "ac1": -0.04111, | |
| 845 | + "ac1_ci95": [ | |
| 846 | + -0.08175, | |
| 847 | + -0.01481 | |
| 848 | + ], | |
| 849 | + "roll_rel_spread": 0.000364, | |
| 850 | + "vr5": 1.008, | |
| 851 | + "vr30": 1.0611, | |
| 852 | + "bucket": "random", | |
| 853 | + "asset": "stock", | |
| 854 | + "xcorr_vs_spy": { | |
| 855 | + "-3": 0.00032, | |
| 856 | + "-2": -0.00068, | |
| 857 | + "-1": 0.00845, | |
| 858 | + "0": 0.14854, | |
| 859 | + "1": 0.07764, | |
| 860 | + "2": 0.02411, | |
| 861 | + "3": -0.01058 | |
| 862 | + }, | |
| 863 | + "spy_leads_+1": 0.07764 | |
| 864 | + } | |
| 865 | + }, | |
| 866 | + "terciles": { | |
| 867 | + "cuts": [ | |
| 868 | + 0.0, | |
| 869 | + 0.3346 | |
| 870 | + ], | |
| 871 | + "agg": { | |
| 872 | + "fresh": { | |
| 873 | + "n": 11, | |
| 874 | + "median_staleness": 0.0, | |
| 875 | + "median_ac1": -0.00916, | |
| 876 | + "median_roll_spread": 0.00014, | |
| 877 | + "median_vr5": 0.9895, | |
| 878 | + "median_vr30": 0.9718, | |
| 879 | + "median_spy_leads_+1": 0.00481 | |
| 880 | + }, | |
| 881 | + "mid": { | |
| 882 | + "n": 10, | |
| 883 | + "median_staleness": 0.1066, | |
| 884 | + "median_ac1": -0.05052, | |
| 885 | + "median_roll_spread": 0.000381, | |
| 886 | + "median_vr5": 0.9264, | |
| 887 | + "median_vr30": 0.9013, | |
| 888 | + "median_spy_leads_+1": 0.04692 | |
| 889 | + }, | |
| 890 | + "stale": { | |
| 891 | + "n": 10, | |
| 892 | + "median_staleness": 0.6912, | |
| 893 | + "median_ac1": -0.23182, | |
| 894 | + "median_roll_spread": 0.001702, | |
| 895 | + "median_vr5": 0.6408, | |
| 896 | + "median_vr30": 0.547, | |
| 897 | + "median_spy_leads_+1": 0.02067 | |
| 898 | + } | |
| 899 | + } | |
| 900 | + }, | |
| 901 | + "staleness_vs_spy_lead_spearman": 0.4287, | |
| 902 | + "spx_vs_spy_xcorr": { | |
| 903 | + "-3": 0.00179, | |
| 904 | + "-2": 0.00012, | |
| 905 | + "-1": 0.06453, | |
| 906 | + "0": 0.96524, | |
| 907 | + "1": 0.00747, | |
| 908 | + "2": -0.00387, | |
| 909 | + "3": -0.00233 | |
| 910 | + }, | |
| 911 | + "client_stats": { | |
| 912 | + "network_requests": 47, | |
| 913 | + "cache_hits": 0, | |
| 914 | + "rows_fetched": 795185, | |
| 915 | + "seconds_waiting": 0.426904123001441, | |
| 916 | + "errors_retried": 0, | |
| 917 | + "refreshes": [] | |
| 918 | + }, | |
| 919 | + "manifest": { | |
| 920 | + "author": "Simon-Pierre Boucher", | |
| 921 | + "contact": "contact@spboucher.ai", | |
| 922 | + "project": "anomaly-atlas", | |
| 923 | + "data_source": "hfmarketdata.io", | |
| 924 | + "collected_utc": "2026-08-12T05:56:30.682310+00:00", | |
| 925 | + "chip": { | |
| 926 | + "brand": "Apple M5 Max", | |
| 927 | + "arch": "arm64", | |
| 928 | + "cores_total": 18, | |
| 929 | + "cores_performance": 6, | |
| 930 | + "cores_efficiency": 12, | |
| 931 | + "gpu_cores": 40 | |
| 932 | + }, | |
| 933 | + "memory": { | |
| 934 | + "unified_bytes": 51539607552, | |
| 935 | + "unified_gb": 48.0, | |
| 936 | + "pagesize": 16384 | |
| 937 | + }, | |
| 938 | + "ssd": { | |
| 939 | + "model": "APPLE SSD AP2048Z", | |
| 940 | + "size": "2 TB", | |
| 941 | + "smart_status": "Verified" | |
| 942 | + }, | |
| 943 | + "os": { | |
| 944 | + "product": "macOS", | |
| 945 | + "version": "27.0", | |
| 946 | + "build": "26A5388g", | |
| 947 | + "kernel": "27.0.0" | |
| 948 | + }, | |
| 949 | + "software": { | |
| 950 | + "python": "3.14.4", | |
| 951 | + "numpy": "2.5.2", | |
| 952 | + "pandas": "3.0.5", | |
| 953 | + "polars": "1.43.2", | |
| 954 | + "duckdb": "1.5.5", | |
| 955 | + "statsmodels": "0.14.6", | |
| 956 | + "arch": "8.0.0" | |
| 957 | + }, | |
| 958 | + "git": { | |
| 959 | + "commit": "f59e89156a980def4efceec445f9a8d23c88513b", | |
| 960 | + "dirty_tree": true | |
| 961 | + } | |
| 962 | + } | |
| 963 | +} | |
modified
src/anomaly_atlas/stats/bootstrap.py
+45 −3
@@ -1,7 +1,7 @@ | ||
| 1 | 1 | # ============================================================================= |
| 2 | 2 | # Project : anomaly-atlas |
| 3 | 3 | # File : src/anomaly_atlas/stats/bootstrap.py |
| 4 | −# Purpose : Block/stationary bootstrap and confidence intervals | |
| 4 | +# Purpose : Moving-block bootstrap and percentile confidence intervals | |
| 5 | 5 | # Author : Simon-Pierre Boucher |
| 6 | 6 | # Contact : contact@spboucher.ai |
| 7 | 7 | # Data src : hfmarketdata.io (sole data source) |
@@ -10,7 +10,49 @@ | ||
| 10 | 10 | # Platform : macOS / Apple Silicon (arm64) |
| 11 | 11 | # License : All rights reserved (research code) |
| 12 | 12 | # ============================================================================= |
| 13 | −"""Block/stationary bootstrap and confidence intervals. | |
| 13 | +"""Moving-block bootstrap for serially dependent data (Künsch 1989). | |
| 14 | 14 | |
| 15 | −Stub scaffolded 2026-08-12; implemented in later phases (see CLAUDE.md). | |
| 15 | +Blocks preserve short-range dependence, so statistics like AC1 or variance | |
| 16 | +ratios get honest sampling distributions. Every call takes an explicit seed. | |
| 16 | 17 | """ |
| 18 | + | |
| 19 | +from __future__ import annotations | |
| 20 | + | |
| 21 | +from collections.abc import Callable | |
| 22 | + | |
| 23 | +import numpy as np | |
| 24 | + | |
| 25 | + | |
| 26 | +def moving_block_bootstrap( | |
| 27 | + x: np.ndarray, | |
| 28 | + stat: Callable[[np.ndarray], float], | |
| 29 | + block: int, | |
| 30 | + n_boot: int = 500, | |
| 31 | + seed: int = 0, | |
| 32 | +) -> np.ndarray: | |
| 33 | + """Bootstrap distribution of `stat` using moving blocks of length `block`.""" | |
| 34 | + x = np.asarray(x, dtype=float) | |
| 35 | + n = len(x) | |
| 36 | + if n < 2 * block: | |
| 37 | + return np.array([]) | |
| 38 | + rng = np.random.default_rng(seed) | |
| 39 | + n_blocks = int(np.ceil(n / block)) | |
| 40 | + starts_max = n - block + 1 | |
| 41 | + out = np.empty(n_boot) | |
| 42 | + for i in range(n_boot): | |
| 43 | + starts = rng.integers(0, starts_max, n_blocks) | |
| 44 | + sample = np.concatenate([x[s : s + block] for s in starts])[:n] | |
| 45 | + out[i] = stat(sample) | |
| 46 | + return out | |
| 47 | + | |
| 48 | + | |
| 49 | +def percentile_ci(samples: np.ndarray, alpha: float = 0.05) -> tuple[float, float]: | |
| 50 | + """Two-sided percentile confidence interval.""" | |
| 51 | + s = np.asarray(samples, dtype=float) | |
| 52 | + s = s[np.isfinite(s)] | |
| 53 | + if len(s) == 0: | |
| 54 | + return (float("nan"), float("nan")) | |
| 55 | + return ( | |
| 56 | + float(np.percentile(s, 100 * alpha / 2)), | |
| 57 | + float(np.percentile(s, 100 * (1 - alpha / 2))), | |
| 58 | + ) | |
modified
src/anomaly_atlas/stats/leadlag.py
+45 −3
@@ -1,7 +1,7 @@ | ||
| 1 | 1 | # ============================================================================= |
| 2 | 2 | # Project : anomaly-atlas |
| 3 | 3 | # File : src/anomaly_atlas/stats/leadlag.py |
| 4 | −# Purpose : Lead-lag tests: cross-correlation, Granger, lagged regressions | |
| 4 | +# Purpose : Lead-lag tests: lagged cross-correlation and asymmetry | |
| 5 | 5 | # Author : Simon-Pierre Boucher |
| 6 | 6 | # Contact : contact@spboucher.ai |
| 7 | 7 | # Data src : hfmarketdata.io (sole data source) |
@@ -10,7 +10,49 @@ | ||
| 10 | 10 | # Platform : macOS / Apple Silicon (arm64) |
| 11 | 11 | # License : All rights reserved (research code) |
| 12 | 12 | # ============================================================================= |
| 13 | −"""Lead-lag tests: cross-correlation, Granger, lagged regressions. | |
| 13 | +"""Lagged cross-correlation between two return series. | |
| 14 | 14 | |
| 15 | −Stub scaffolded 2026-08-12; implemented in later phases (see CLAUDE.md). | |
| 15 | +Sign convention: lag k > 0 means x LEADS y by k bars — corr(x_{t-k}, y_t). | |
| 16 | +Validated on synthetic ground truth (charter §8.1) before real data. | |
| 16 | 17 | """ |
| 18 | + | |
| 19 | +from __future__ import annotations | |
| 20 | + | |
| 21 | +import numpy as np | |
| 22 | + | |
| 23 | + | |
| 24 | +def lagged_xcorr(x: np.ndarray, y: np.ndarray, max_lag: int) -> dict[int, float]: | |
| 25 | + """corr(x_{t-k}, y_t) for k in [-max_lag, +max_lag]; k>0 = x leads y.""" | |
| 26 | + x = np.asarray(x, dtype=float) | |
| 27 | + y = np.asarray(y, dtype=float) | |
| 28 | + n = min(len(x), len(y)) | |
| 29 | + x, y = x[:n], y[:n] | |
| 30 | + out: dict[int, float] = {} | |
| 31 | + for k in range(-max_lag, max_lag + 1): | |
| 32 | + if k >= 0: | |
| 33 | + a, b = x[: n - k] if k else x, y[k:] if k else y | |
| 34 | + else: | |
| 35 | + a, b = x[-k:], y[: n + k] | |
| 36 | + if len(a) < 3 or a.std() == 0.0 or b.std() == 0.0: | |
| 37 | + out[k] = float("nan") | |
| 38 | + continue | |
| 39 | + out[k] = float(np.corrcoef(a, b)[0, 1]) | |
| 40 | + return out | |
| 41 | + | |
| 42 | + | |
| 43 | +def peak_lag(xc: dict[int, float]) -> int: | |
| 44 | + """Lag with the largest |corr| (ties: smallest |lag|).""" | |
| 45 | + finite = {k: v for k, v in xc.items() if np.isfinite(v)} | |
| 46 | + if not finite: | |
| 47 | + return 0 | |
| 48 | + return min(finite, key=lambda k: (-abs(finite[k]), abs(k))) | |
| 49 | + | |
| 50 | + | |
| 51 | +def leadlag_asymmetry(xc: dict[int, float]) -> float: | |
| 52 | + """Sum of corr at positive lags minus sum at negative lags. | |
| 53 | + | |
| 54 | + Zero (in expectation) for synchronous series; positive when x leads y. | |
| 55 | + """ | |
| 56 | + pos = sum(v for k, v in xc.items() if k > 0 and np.isfinite(v)) | |
| 57 | + neg = sum(v for k, v in xc.items() if k < 0 and np.isfinite(v)) | |
| 58 | + return float(pos - neg) | |
modified
src/anomaly_atlas/stats/reversion.py
+70 −3
@@ -1,7 +1,7 @@ | ||
| 1 | 1 | # ============================================================================= |
| 2 | 2 | # Project : anomaly-atlas |
| 3 | 3 | # File : src/anomaly_atlas/stats/reversion.py |
| 4 | −# Purpose : Mean-reversion tests: variance ratios, Hurst, AR, half-life | |
| 4 | +# Purpose : Mean-reversion tests: variance ratios, AC1, AR half-life | |
| 5 | 5 | # Author : Simon-Pierre Boucher |
| 6 | 6 | # Contact : contact@spboucher.ai |
| 7 | 7 | # Data src : hfmarketdata.io (sole data source) |
@@ -10,7 +10,74 @@ | ||
| 10 | 10 | # Platform : macOS / Apple Silicon (arm64) |
| 11 | 11 | # License : All rights reserved (research code) |
| 12 | 12 | # ============================================================================= |
| 13 | −"""Mean-reversion tests: variance ratios, Hurst, AR, half-life. | |
| 13 | +"""Mean-reversion statistics on return series. | |
| 14 | 14 | |
| 15 | −Stub scaffolded 2026-08-12; implemented in later phases (see CLAUDE.md). | |
| 15 | +Validated on synthetic ground truth before touching real data | |
| 16 | +(benchmarks/synthetic/test_synthetic_gate.py — charter §8.1). | |
| 16 | 17 | """ |
| 18 | + | |
| 19 | +from __future__ import annotations | |
| 20 | + | |
| 21 | +import numpy as np | |
| 22 | + | |
| 23 | + | |
| 24 | +def ac1(returns: np.ndarray) -> float: | |
| 25 | + """Lag-1 autocorrelation of a return series.""" | |
| 26 | + r = np.asarray(returns, dtype=float) | |
| 27 | + if len(r) < 3: | |
| 28 | + return float("nan") | |
| 29 | + a, b = r[:-1], r[1:] | |
| 30 | + sa, sb = a.std(), b.std() | |
| 31 | + if sa == 0.0 or sb == 0.0: | |
| 32 | + return float("nan") | |
| 33 | + return float(((a - a.mean()) * (b - b.mean())).mean() / (sa * sb)) | |
| 34 | + | |
| 35 | + | |
| 36 | +def autocov1(returns: np.ndarray) -> float: | |
| 37 | + """Lag-1 autocovariance (input to the Roll spread estimator).""" | |
| 38 | + r = np.asarray(returns, dtype=float) | |
| 39 | + if len(r) < 3: | |
| 40 | + return float("nan") | |
| 41 | + a, b = r[:-1], r[1:] | |
| 42 | + return float(((a - a.mean()) * (b - b.mean())).mean()) | |
| 43 | + | |
| 44 | + | |
| 45 | +def variance_ratio(returns: np.ndarray, q: int) -> float: | |
| 46 | + """Lo-MacKinlay variance ratio VR(q) with overlapping q-period sums. | |
| 47 | + | |
| 48 | + Ground truth: VR = 1 for a random walk, < 1 under mean reversion, | |
| 49 | + > 1 under momentum. Unbiased variance estimators, demeaned. | |
| 50 | + """ | |
| 51 | + r = np.asarray(returns, dtype=float) | |
| 52 | + n = len(r) | |
| 53 | + if n < q + 2 or q < 2: | |
| 54 | + return float("nan") | |
| 55 | + mu = r.mean() | |
| 56 | + var1 = ((r - mu) ** 2).sum() / (n - 1) | |
| 57 | + rq = np.convolve(r, np.ones(q), mode="valid") # overlapping q-sums | |
| 58 | + # Lo-MacKinlay bias-corrected PER-PERIOD variance of q-sums: the factor q | |
| 59 | + # lives inside m, so the ratio below is varq/var1 (NOT varq/(q*var1)). | |
| 60 | + m = q * (n - q + 1) * (1 - q / n) | |
| 61 | + varq = ((rq - q * mu) ** 2).sum() / m | |
| 62 | + if var1 == 0.0: | |
| 63 | + return float("nan") | |
| 64 | + return float(varq / var1) | |
| 65 | + | |
| 66 | + | |
| 67 | +def half_life(log_prices: np.ndarray) -> float: | |
| 68 | + """Mean-reversion half-life from an AR(1) fit: dp_t = a + b*p_{t-1} + e. | |
| 69 | + | |
| 70 | + Returns ln(2)/-ln(1+b) in bars for b in (-1, 0); +inf if b >= 0 | |
| 71 | + (no reversion). Matches the OU generator's ln(2)/-ln(1-kappa). | |
| 72 | + """ | |
| 73 | + p = np.asarray(log_prices, dtype=float) | |
| 74 | + if len(p) < 10: | |
| 75 | + return float("nan") | |
| 76 | + x, y = p[:-1], np.diff(p) | |
| 77 | + vx = x.var() | |
| 78 | + if vx == 0.0: | |
| 79 | + return float("nan") | |
| 80 | + b = ((x - x.mean()) * (y - y.mean())).mean() / vx | |
| 81 | + if b >= 0.0 or b <= -1.0: | |
| 82 | + return float("inf") | |
| 83 | + return float(np.log(2.0) / -np.log1p(b)) | |
modified
src/anomaly_atlas/validation/artifacts.py
+84 −3
@@ -1,7 +1,7 @@ | ||
| 1 | 1 | # ============================================================================= |
| 2 | 2 | # Project : anomaly-atlas |
| 3 | 3 | # File : src/anomaly_atlas/validation/artifacts.py |
| 4 | −# Purpose : Artifact detectors: bid-ask bounce, staleness, look-ahead | |
| 4 | +# Purpose : Artifact detectors: Roll bounce, staleness, LOCF resampling | |
| 5 | 5 | # Author : Simon-Pierre Boucher |
| 6 | 6 | # Contact : contact@spboucher.ai |
| 7 | 7 | # Data src : hfmarketdata.io (sole data source) |
@@ -10,7 +10,88 @@ | ||
| 10 | 10 | # Platform : macOS / Apple Silicon (arm64) |
| 11 | 11 | # License : All rights reserved (research code) |
| 12 | 12 | # ============================================================================= |
| 13 | −"""Artifact detectors: bid-ask bounce, staleness, look-ahead. | |
| 13 | +"""Detectors for the mechanisms that manufacture fake anomalies in bar data. | |
| 14 | 14 | |
| 15 | −Stub scaffolded 2026-08-12; implemented in later phases (see CLAUDE.md). | |
| 15 | +Doctrine (charter §2.1): every candidate anomaly must first be explained by | |
| 16 | +these nulls before it may be called a regularity. Each function is validated | |
| 17 | +on synthetic ground truth (charter §8.1). | |
| 16 | 18 | """ |
| 19 | + | |
| 20 | +from __future__ import annotations | |
| 21 | + | |
| 22 | +import numpy as np | |
| 23 | + | |
| 24 | +from anomaly_atlas.stats.reversion import autocov1 | |
| 25 | + | |
| 26 | + | |
| 27 | +def roll_spread(returns: np.ndarray) -> float: | |
| 28 | + """Roll (1984) implied effective spread: 2*sqrt(-Cov(r_t, r_{t-1})). | |
| 29 | + | |
| 30 | + In log-return space this is the RELATIVE spread. Returns NaN when the | |
| 31 | + lag-1 autocovariance is non-negative (estimator undefined — typical for | |
| 32 | + momentum or noise-free series). | |
| 33 | + """ | |
| 34 | + cov = autocov1(returns) | |
| 35 | + if not np.isfinite(cov) or cov >= 0.0: | |
| 36 | + return float("nan") | |
| 37 | + return float(2.0 * np.sqrt(-cov)) | |
| 38 | + | |
| 39 | + | |
| 40 | +def bounce_implied_ac1(returns: np.ndarray) -> float: | |
| 41 | + """The lag-1 autocorrelation a pure Roll bounce would produce for this | |
| 42 | + series: -s^2/4 divided by Var(r), with s the Roll implied spread. | |
| 43 | + | |
| 44 | + Because s is estimated FROM the lag-1 autocovariance, this equals the | |
| 45 | + measured AC1 whenever AC1 < 0 — the useful output is the DECOMPOSITION: | |
| 46 | + ``excess_reversion`` reports how much reversion remains after removing | |
| 47 | + the bounce explainable by the observed spread level. | |
| 48 | + """ | |
| 49 | + r = np.asarray(returns, dtype=float) | |
| 50 | + s = roll_spread(r) | |
| 51 | + if not np.isfinite(s): | |
| 52 | + return 0.0 | |
| 53 | + var = r.var() | |
| 54 | + if var == 0.0: | |
| 55 | + return float("nan") | |
| 56 | + return float(-(s**2) / 4.0 / var) | |
| 57 | + | |
| 58 | + | |
| 59 | +def excess_reversion(returns: np.ndarray, rel_spread: float) -> float: | |
| 60 | + """Artifact-adjusted AC1: measured AC1 minus the bounce null implied by an | |
| 61 | + INDEPENDENT spread estimate ``rel_spread`` (e.g. a liquidity-matched | |
| 62 | + spread level, or a quoted/estimated spread from another source). | |
| 63 | + | |
| 64 | + For a pure Roll series with the true spread supplied, this is ≈ 0. | |
| 65 | + A genuinely mean-reverting series keeps a negative excess. | |
| 66 | + """ | |
| 67 | + r = np.asarray(returns, dtype=float) | |
| 68 | + var = r.var() | |
| 69 | + if var == 0.0 or len(r) < 3: | |
| 70 | + return float("nan") | |
| 71 | + from anomaly_atlas.stats.reversion import ac1 | |
| 72 | + | |
| 73 | + bounce_ac1 = -(rel_spread**2) / 4.0 / var | |
| 74 | + return float(ac1(r) - bounce_ac1) | |
| 75 | + | |
| 76 | + | |
| 77 | +def staleness_ratio(observed_mask: np.ndarray) -> float: | |
| 78 | + """Fraction of grid slots WITHOUT a fresh print (0 = fully fresh).""" | |
| 79 | + m = np.asarray(observed_mask, dtype=bool) | |
| 80 | + if len(m) == 0: | |
| 81 | + return float("nan") | |
| 82 | + return float(1.0 - m.mean()) | |
| 83 | + | |
| 84 | + | |
| 85 | +def locf_fill(values: np.ndarray, observed_mask: np.ndarray) -> np.ndarray: | |
| 86 | + """Last-observation-carried-forward fill of a gridded series. | |
| 87 | + | |
| 88 | + Slots before the first observation keep their original value. This is | |
| 89 | + the (dangerous) join that manufactures stale-price artifacts — it exists | |
| 90 | + here so experiments can measure that artifact explicitly. | |
| 91 | + """ | |
| 92 | + v = np.asarray(values, dtype=float).copy() | |
| 93 | + m = np.asarray(observed_mask, dtype=bool) | |
| 94 | + for t in range(1, len(v)): | |
| 95 | + if not m[t]: | |
| 96 | + v[t] = v[t - 1] | |
| 97 | + return v | |
| 17 | 98 | |