SPB Git

spb/anomaly-atlas Public License

Systematic discovery & rigorous validation of statistical anomalies in open HF market data (hfmarketdata.io) — pre-registered, artifact-null-driven, fully reproducible. Live atlas: www.anomaly-atlas.io

Python 61.4% JavaScript 28.7% CSS 8.6% Shell 0.7% Makefile 0.5%
9.2 KB · 265 lines python
Raw Blame History
1# =============================================================================2#  Project   : anomaly-atlas3#  File      : experiments/micro/expA_data_reality/benchmark.py4#  Purpose   : Benchmark runner: empirical data reality check of hfmarketdata.io5#  Author    : Simon-Pierre Boucher6#  Contact   : contact@spboucher.ai7#  Data src  : hfmarketdata.io (sole data source)8#  Created   : 2026-08-129#  Modified  : 2026-08-1210#  Platform  : macOS / Apple Silicon (arm64)11#  License   : All rights reserved (research code)12# =============================================================================13"""Experiment A — establish empirically what hfmarketdata.io actually returns.1415Probes (all through the single hf_client, so every response is cached and16manifest-indexed):17  A1  dataset inventory (/v1/status): assets x timeframes x adjustments18  A2  history bounds per asset class (earliest/latest 1min and 1day)19  A3  intraday session structure and missing-minute patterns (liquid vs sparse)20  A4  daily-bar vs 1min-aggregate semantics (official close vs last bar)21  A5  corporate-action adjustment semantics around the AAPL 2020 4:1 split22  A6  response row cap + pagination correctness on a full year of 1min bars23  A7  request latency profile (small cached-miss requests)24  A8  options coverage (quarters, expirations, chain columns)2526Writes results/expA_data_reality/<UTC timestamp>/results.json embedding the27hardware manifest and the client's instrumentation stats.28"""2930from __future__ import annotations3132import json33import sys34import time35from collections import Counter36from datetime import UTC, datetime37from pathlib import Path3839REPO_ROOT = Path(__file__).resolve().parents[3]40sys.path.insert(0, str(REPO_ROOT / "benchmarks"))41sys.path.insert(0, str(REPO_ROOT / "src"))4243from hardware_manifest import collect_manifest  # noqa: E4024445from anomaly_atlas.data.hf_client import HFMarketDataClient  # noqa: E4024647REPRESENTATIVE = {48    "stock": ("AAPL", "adj_splitdiv"),49    "etf": ("SPY", "adj_splitdiv"),50    "futures": ("ES", "contin_adj_ratio"),51    "index": ("SPX", None),52    "fx": ("EURUSD", None),53    "crypto": ("BTC", None),54}55PROBE_DAY = "2026-08-06"  # a regular Thursday inside every dataset's coverage565758def bounds(59    client: HFMarketDataClient, asset: str, ticker: str, adjustment: str | None, timeframe: str60) -> dict:61    def one(order: str) -> str | None:62        rows = client.get(63            f"/v1/bars/{asset}/{ticker}",64            {65                "timeframe": timeframe,66                "adjustment": adjustment,67                "order": order,68                "limit": 1,69            },70        ).get("data", [])71        return rows[0]["datetime"] if rows else None7273    return {"earliest": one("asc"), "latest": one("desc")}747576def session_structure(77    client: HFMarketDataClient, asset: str, ticker: str, adjustment: str | None78) -> dict:79    rows = client.get(80        f"/v1/bars/{asset}/{ticker}",81        {82            "timeframe": "1min",83            "adjustment": adjustment,84            "start": PROBE_DAY,85            "end": "2026-08-07",86            "order": "asc",87            "limit": 50_000,88        },89    ).get("data", [])90    rows = [r for r in rows if r["datetime"][:10] == PROBE_DAY]91    if not rows:92        return {"bars": 0}93    per_hour = Counter(r["datetime"][11:13] for r in rows)94    rth = [r for r in rows if "09:30" <= r["datetime"][11:16] < "16:00"]95    return {96        "bars": len(rows),97        "first": rows[0]["datetime"],98        "last": rows[-1]["datetime"],99        "bars_per_hour": dict(sorted(per_hour.items())),100        "zero_volume_bars": sum(1 for r in rows if r.get("volume") == 0),101        "has_volume_field": "volume" in rows[0],102        "rth_bars_of_390": len(rth),103    }104105106def main() -> None:107    run_utc = datetime.now(UTC)108    client = HFMarketDataClient()109    results: dict = {110        "experiment": "expA_data_reality",111        "run_utc": run_utc.isoformat(),112        "author": "Simon-Pierre Boucher",113        "contact": "contact@spboucher.ai",114        "data_source": "hfmarketdata.io",115    }116117    # A1 — inventory118    status = client.status()119    results["A1_inventory"] = status.get("datasets", {})120    results["A1_options_quarters"] = (121        status.get("datasets", {}).get("options", {}).get("quarters", [])122    )123124    # A2 — history bounds per class, 1min and 1day125    results["A2_bounds"] = {126        asset: {tf: bounds(client, asset, tk, adj, tf) for tf in ("1min", "1day")}127        for asset, (tk, adj) in REPRESENTATIVE.items()128    }129130    # A3 — session structure, liquid + sparse131    results["A3_session"] = {132        f"{asset}:{tk}": session_structure(client, asset, tk, adj)133        for asset, (tk, adj) in REPRESENTATIVE.items()134    }135    results["A3_session"]["stock:AIZN(sparse)"] = session_structure(136        client, "stock", "AIZN", "adj_splitdiv"137    )138139    # A4 — daily bar vs 1min RTH aggregate (close semantics)140    day = client.get(141        "/v1/bars/stock/AAPL",142        {143            "timeframe": "1day",144            "adjustment": "adj_splitdiv",145            "start": PROBE_DAY,146            "end": "2026-08-07",147        },148    )["data"][0]149    intraday = client.get(150        "/v1/bars/stock/AAPL",151        {152            "timeframe": "1min",153            "adjustment": "adj_splitdiv",154            "start": PROBE_DAY,155            "end": "2026-08-07",156            "order": "asc",157            "limit": 50_000,158        },159    )["data"]160    rth = [r for r in intraday if "09:30" <= r["datetime"][11:16] < "16:00"]161    results["A4_daily_vs_intraday"] = {162        "daily_bar": day,163        "rth_1min_aggregate": {164            "open": rth[0]["open"],165            "high": max(r["high"] for r in rth),166            "low": min(r["low"] for r in rth),167            "close": rth[-1]["close"],168            "volume": sum(r["volume"] for r in rth),169            "bars": len(rth),170        },171        "extended_1min_volume": sum(r["volume"] for r in intraday),172    }173174    # A5 — adjustment semantics around AAPL 2020-08-31 4:1 split175    results["A5_adjustments"] = {}176    for adj in ("UNADJUSTED", "adj_split", "adj_splitdiv"):177        rows = client.get(178            "/v1/bars/stock/AAPL",179            {180                "timeframe": "1day",181                "adjustment": adj,182                "start": "2020-08-28",183                "end": "2020-09-01",184            },185        )["data"]186        results["A5_adjustments"][adj] = [187            {"date": r["datetime"][:10], "close": r["close"]} for r in rows188        ]189190    # A6 — row cap + pagination on a full year of SPY 1min191    capped = client.get(192        "/v1/bars/etf/SPY",193        {194            "timeframe": "1min",195            "adjustment": "adj_splitdiv",196            "start": "2020-01-01",197            "end": "2021-01-01",198            "order": "asc",199            "limit": 1_000_000,200        },201    )202    year = client.get_bars("etf", "SPY", "1min", "adj_splitdiv", "2020-01-01", "2021-01-01")203    dts = [r["datetime"] for r in year]204    results["A6_row_cap_and_pagination"] = {205        "requested_rows": 1_000_000,206        "returned_rows_single_request": capped.get("count"),207        "paginated_total_rows": len(year),208        "paginated_first": dts[0],209        "paginated_last": dts[-1],210        "duplicates_after_pagination": len(dts) - len(set(dts)),211        "monotonic_ascending": all(a < b for a, b in zip(dts, dts[1:], strict=False)),212    }213214    # A7 — latency profile (cache misses: distinct small requests)215    latencies = []216    for month in range(1, 11):217        t0 = time.perf_counter()218        client.get(219            "/v1/bars/stock/MSFT",220            {221                "timeframe": "1day",222                "adjustment": "adj_splitdiv",223                "start": f"2025-{month:02d}-01",224                "end": f"2025-{month:02d}-05",225            },226        )227        latencies.append(round(time.perf_counter() - t0, 4))228    results["A7_latency_s"] = {229        "samples": latencies,230        "note": "sequential small requests, cold cache; no rate-limit headers observed",231    }232233    # A8 — options coverage234    expiries = client.options_expirations("SPY", trade_date="2026-06-15")235    chain = client.options_chain("SPY", "2026-06-15", limit=2)236    results["A8_options"] = {237        "n_quarters": len(results["A1_options_quarters"]),238        "first_quarter": results["A1_options_quarters"][:1],239        "last_quarter": results["A1_options_quarters"][-1:],240        "spy_expirations_on_2026-06-15": len(expiries),241        "chain_columns": sorted(chain[0].keys()) if chain else None,242        "granularity": "daily (one row per contract per trade_date)",243    }244245    # instrumentation + hardware246    results["client_stats"] = {247        "network_requests": client.stats.network_requests,248        "cache_hits": client.stats.cache_hits,249        "rows_fetched": client.stats.rows_fetched,250        "seconds_waiting": round(client.stats.seconds_waiting, 2),251        "errors_retried": client.stats.errors_retried,252    }253    results["manifest"] = collect_manifest()254255    out_dir = REPO_ROOT / "results" / "expA_data_reality" / run_utc.strftime("%Y%m%dT%H%M%SZ")256    out_dir.mkdir(parents=True)257    out = out_dir / "results.json"258    out.write_text(json.dumps(results, indent=2) + "\n")259    print(f"wrote {out.relative_to(REPO_ROOT)}")260    print(json.dumps(results["client_stats"], indent=2))261262263if __name__ == "__main__":264    main()265