Honesty doctrine. Every candidate anomaly is an artifact until proven otherwise; in-sample results are never findings; past statistical regularity does not imply future returns. This is research on statistical properties of market data — not investment advice, not a trading system.

Code / experiments/micro/expA_data_reality/benchmark.py

experiments/micro/expA_data_reality/benchmark.py 265 lines
# =============================================================================
#  Project   : anomaly-atlas
#  File      : experiments/micro/expA_data_reality/benchmark.py
#  Purpose   : Benchmark runner: empirical data reality check of hfmarketdata.io
#  Author    : Simon-Pierre Boucher
#  Contact   : contact@spboucher.ai
#  Data src  : hfmarketdata.io (sole data source)
#  Created   : 2026-08-12
#  Modified  : 2026-08-12
#  Platform  : macOS / Apple Silicon (arm64)
#  License   : All rights reserved (research code)
# =============================================================================
"""Experiment A — establish empirically what hfmarketdata.io actually returns.

Probes (all through the single hf_client, so every response is cached and
manifest-indexed):
  A1  dataset inventory (/v1/status): assets x timeframes x adjustments
  A2  history bounds per asset class (earliest/latest 1min and 1day)
  A3  intraday session structure and missing-minute patterns (liquid vs sparse)
  A4  daily-bar vs 1min-aggregate semantics (official close vs last bar)
  A5  corporate-action adjustment semantics around the AAPL 2020 4:1 split
  A6  response row cap + pagination correctness on a full year of 1min bars
  A7  request latency profile (small cached-miss requests)
  A8  options coverage (quarters, expirations, chain columns)

Writes results/expA_data_reality/<UTC timestamp>/results.json embedding the
hardware manifest and the client's instrumentation stats.
"""

from __future__ import annotations

import json
import sys
import time
from collections import Counter
from datetime import UTC, datetime
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(REPO_ROOT / "benchmarks"))
sys.path.insert(0, str(REPO_ROOT / "src"))

from hardware_manifest import collect_manifest  # noqa: E402

from anomaly_atlas.data.hf_client import HFMarketDataClient  # noqa: E402

REPRESENTATIVE = {
    "stock": ("AAPL", "adj_splitdiv"),
    "etf": ("SPY", "adj_splitdiv"),
    "futures": ("ES", "contin_adj_ratio"),
    "index": ("SPX", None),
    "fx": ("EURUSD", None),
    "crypto": ("BTC", None),
}
PROBE_DAY = "2026-08-06"  # a regular Thursday inside every dataset's coverage


def bounds(
    client: HFMarketDataClient, asset: str, ticker: str, adjustment: str | None, timeframe: str
) -> dict:
    def one(order: str) -> str | None:
        rows = client.get(
            f"/v1/bars/{asset}/{ticker}",
            {
                "timeframe": timeframe,
                "adjustment": adjustment,
                "order": order,
                "limit": 1,
            },
        ).get("data", [])
        return rows[0]["datetime"] if rows else None

    return {"earliest": one("asc"), "latest": one("desc")}


def session_structure(
    client: HFMarketDataClient, asset: str, ticker: str, adjustment: str | None
) -> dict:
    rows = client.get(
        f"/v1/bars/{asset}/{ticker}",
        {
            "timeframe": "1min",
            "adjustment": adjustment,
            "start": PROBE_DAY,
            "end": "2026-08-07",
            "order": "asc",
            "limit": 50_000,
        },
    ).get("data", [])
    rows = [r for r in rows if r["datetime"][:10] == PROBE_DAY]
    if not rows:
        return {"bars": 0}
    per_hour = Counter(r["datetime"][11:13] for r in rows)
    rth = [r for r in rows if "09:30" <= r["datetime"][11:16] < "16:00"]
    return {
        "bars": len(rows),
        "first": rows[0]["datetime"],
        "last": rows[-1]["datetime"],
        "bars_per_hour": dict(sorted(per_hour.items())),
        "zero_volume_bars": sum(1 for r in rows if r.get("volume") == 0),
        "has_volume_field": "volume" in rows[0],
        "rth_bars_of_390": len(rth),
    }


def main() -> None:
    run_utc = datetime.now(UTC)
    client = HFMarketDataClient()
    results: dict = {
        "experiment": "expA_data_reality",
        "run_utc": run_utc.isoformat(),
        "author": "Simon-Pierre Boucher",
        "contact": "contact@spboucher.ai",
        "data_source": "hfmarketdata.io",
    }

    # A1 — inventory
    status = client.status()
    results["A1_inventory"] = status.get("datasets", {})
    results["A1_options_quarters"] = (
        status.get("datasets", {}).get("options", {}).get("quarters", [])
    )

    # A2 — history bounds per class, 1min and 1day
    results["A2_bounds"] = {
        asset: {tf: bounds(client, asset, tk, adj, tf) for tf in ("1min", "1day")}
        for asset, (tk, adj) in REPRESENTATIVE.items()
    }

    # A3 — session structure, liquid + sparse
    results["A3_session"] = {
        f"{asset}:{tk}": session_structure(client, asset, tk, adj)
        for asset, (tk, adj) in REPRESENTATIVE.items()
    }
    results["A3_session"]["stock:AIZN(sparse)"] = session_structure(
        client, "stock", "AIZN", "adj_splitdiv"
    )

    # A4 — daily bar vs 1min RTH aggregate (close semantics)
    day = client.get(
        "/v1/bars/stock/AAPL",
        {
            "timeframe": "1day",
            "adjustment": "adj_splitdiv",
            "start": PROBE_DAY,
            "end": "2026-08-07",
        },
    )["data"][0]
    intraday = client.get(
        "/v1/bars/stock/AAPL",
        {
            "timeframe": "1min",
            "adjustment": "adj_splitdiv",
            "start": PROBE_DAY,
            "end": "2026-08-07",
            "order": "asc",
            "limit": 50_000,
        },
    )["data"]
    rth = [r for r in intraday if "09:30" <= r["datetime"][11:16] < "16:00"]
    results["A4_daily_vs_intraday"] = {
        "daily_bar": day,
        "rth_1min_aggregate": {
            "open": rth[0]["open"],
            "high": max(r["high"] for r in rth),
            "low": min(r["low"] for r in rth),
            "close": rth[-1]["close"],
            "volume": sum(r["volume"] for r in rth),
            "bars": len(rth),
        },
        "extended_1min_volume": sum(r["volume"] for r in intraday),
    }

    # A5 — adjustment semantics around AAPL 2020-08-31 4:1 split
    results["A5_adjustments"] = {}
    for adj in ("UNADJUSTED", "adj_split", "adj_splitdiv"):
        rows = client.get(
            "/v1/bars/stock/AAPL",
            {
                "timeframe": "1day",
                "adjustment": adj,
                "start": "2020-08-28",
                "end": "2020-09-01",
            },
        )["data"]
        results["A5_adjustments"][adj] = [
            {"date": r["datetime"][:10], "close": r["close"]} for r in rows
        ]

    # A6 — row cap + pagination on a full year of SPY 1min
    capped = client.get(
        "/v1/bars/etf/SPY",
        {
            "timeframe": "1min",
            "adjustment": "adj_splitdiv",
            "start": "2020-01-01",
            "end": "2021-01-01",
            "order": "asc",
            "limit": 1_000_000,
        },
    )
    year = client.get_bars("etf", "SPY", "1min", "adj_splitdiv", "2020-01-01", "2021-01-01")
    dts = [r["datetime"] for r in year]
    results["A6_row_cap_and_pagination"] = {
        "requested_rows": 1_000_000,
        "returned_rows_single_request": capped.get("count"),
        "paginated_total_rows": len(year),
        "paginated_first": dts[0],
        "paginated_last": dts[-1],
        "duplicates_after_pagination": len(dts) - len(set(dts)),
        "monotonic_ascending": all(a < b for a, b in zip(dts, dts[1:], strict=False)),
    }

    # A7 — latency profile (cache misses: distinct small requests)
    latencies = []
    for month in range(1, 11):
        t0 = time.perf_counter()
        client.get(
            "/v1/bars/stock/MSFT",
            {
                "timeframe": "1day",
                "adjustment": "adj_splitdiv",
                "start": f"2025-{month:02d}-01",
                "end": f"2025-{month:02d}-05",
            },
        )
        latencies.append(round(time.perf_counter() - t0, 4))
    results["A7_latency_s"] = {
        "samples": latencies,
        "note": "sequential small requests, cold cache; no rate-limit headers observed",
    }

    # A8 — options coverage
    expiries = client.options_expirations("SPY", trade_date="2026-06-15")
    chain = client.options_chain("SPY", "2026-06-15", limit=2)
    results["A8_options"] = {
        "n_quarters": len(results["A1_options_quarters"]),
        "first_quarter": results["A1_options_quarters"][:1],
        "last_quarter": results["A1_options_quarters"][-1:],
        "spy_expirations_on_2026-06-15": len(expiries),
        "chain_columns": sorted(chain[0].keys()) if chain else None,
        "granularity": "daily (one row per contract per trade_date)",
    }

    # instrumentation + hardware
    results["client_stats"] = {
        "network_requests": client.stats.network_requests,
        "cache_hits": client.stats.cache_hits,
        "rows_fetched": client.stats.rows_fetched,
        "seconds_waiting": round(client.stats.seconds_waiting, 2),
        "errors_retried": client.stats.errors_retried,
    }
    results["manifest"] = collect_manifest()

    out_dir = REPO_ROOT / "results" / "expA_data_reality" / run_utc.strftime("%Y%m%dT%H%M%SZ")
    out_dir.mkdir(parents=True)
    out = out_dir / "results.json"
    out.write_text(json.dumps(results, indent=2) + "\n")
    print(f"wrote {out.relative_to(REPO_ROOT)}")
    print(json.dumps(results["client_stats"], indent=2))


if __name__ == "__main__":
    main()