SPB Git forge

spb/hfmarketdata

Public

Open high-frequency market data platform — FirstRate full-history downloader, DuckDB/Parquet lake, open REST API and React docs platform (www.hfmarketdata.io)

127commits 1branches 0releases
24.7 MBsize
maindefault branch
11 days agolast push
JavaScript 53.7% Python 38.3% CSS 4.6% TypeScript 3.1%
22.6 KB · 385 lines python
Raw Blame History
1"""Integration tests — backfill + the 7 `/v1/futures/*` endpoints on the synthetic lake."""2from __future__ import annotations34import io5import os6import subprocess7import sys8from datetime import date9from pathlib import Path1011import pandas as pd12import pytest1314TODAY = date(2025, 7, 1)   # fixture lake ends 2025-06-30151617@pytest.fixture(scope="module")18def backfilled(app, lake):19    from futures.backfill import run_backfill20    s1 = run_backfill(today=TODAY)21    s2 = run_backfill(today=TODAY)          # idempotent22    assert s1["contracts"] == s2["contracts"] > 023    return s1242526# ---- backfill ------------------------------------------------------------------------------------------------2728def test_backfill_summary_and_tables(backfilled, app):29    from core.db import session30    from futures.models import FuturesContract, FuturesRoot31    from sqlalchemy import select32    assert backfilled["rule_based"] == backfilled["contracts"]        # ES, CL, NG, E6 all have exchange rules33    assert backfilled["active"] > 0 and backfilled["seconds"] < 3034    with session() as s:35        es = s.execute(select(FuturesContract).where(FuturesContract.symbol == "ESH25")).scalar_one()36        assert es.expiration_date == date(2025, 3, 21) and es.expiration_source == "rule"37        assert es.first_data_date is not None and es.last_data_date == date(2025, 3, 21)38        assert es.status == "expired" and es.volume_avg_daily > 0 and es.open_interest_last > 039        cl = s.execute(select(FuturesContract).where(FuturesContract.symbol == "CLJ25")).scalar_one()40        assert cl.first_notice_date == date(2025, 3, 21) and cl.expiration_date == date(2025, 3, 20)41        root = s.get(FuturesRoot, "ES")42        assert root.source == "reference" and root.contracts_count >= 14 and root.exchange == "CME"43        zk = s.get(FuturesRoot, "ZK")                                  # from meta csv only → derived44        assert zk.source == "derived" and zk.name == "Unknown Product" and zk.exchange == "XXX" and zk.expiry_rule == "data"45        e6 = s.get(FuturesRoot, "E6")46        assert '"6E"' in e6.aliases474849def test_backfill_partial_roots(backfilled, app):50    from futures.backfill import run_backfill51    s = run_backfill(roots=["6E"], today=TODAY)          # alias accepted through normalize in CLI; here lake root E6 expected52    assert s["contracts"] == 0                            # "6E" is not a lake root: nothing scanned…53    s = run_backfill(roots=["E6"], today=TODAY)54    assert s["contracts"] >= 8 and s["roots"] == 1555657def test_backfill_cli(lake):58    env = {**os.environ, "HFMD_DATA_ROOT": str(lake), "HFMD_STATE_DB": str(lake / "state" / "hfmd.db"), "HFMD_ENV": "test"}59    script = Path(__file__).resolve().parents[1] / "scripts" / "backfill_contracts.py"60    r = subprocess.run([sys.executable, str(script), "--roots", "6E,ES", "--today", str(TODAY), "--json"], env=env,61                       capture_output=True, text=True, timeout=120, check=False)62    assert r.returncode == 0, r.stderr63    import json64    out = json.loads(r.stdout.strip().splitlines()[-1])65    assert out["contracts"] >= 20 and out["rule_based"] == out["contracts"]666768# ---- roots / contracts ----------------------------------------------------------------------------------------6970def test_roots(client, backfilled):71    r = client.get("/v1/futures/roots")72    assert r.status_code == 20073    body = r.json()74    roots = {x["root"]: x for x in body["data"]}75    assert body["meta"]["count"] == len(roots) >= 12076    assert roots["ES"]["name"] == "E-mini S&P 500" and roots["ES"]["tick_value"] == 12.5 and roots["ES"]["expiry_rule"] == "third_friday"77    assert roots["ES"]["contracts_count"] >= 14 and roots["ES"]["first_data_date"] and roots["ES"]["source"] == "reference"78    assert roots["E6"]["aliases"] == ["6E"] and roots["US"]["aliases"] == ["ZB"]79    assert roots["ZK"]["source"] == "derived" and roots["ZK"]["asset_class"] is None80    r = client.get("/v1/futures/roots?asset_class=energy&search=crude")81    assert {x["root"] for x in r.json()["data"]} == {"CL", "MCL", "B"}82    r = client.get("/v1/futures/roots?exchange=NYMEX")83    assert {x["root"] for x in r.json()["data"]} >= {"CL", "BZ", "NG", "HO", "RB"}84    r = client.get("/v1/futures/roots?format=csv")85    assert r.status_code == 200 and r.text.startswith("root,name,exchange")868788def test_contracts_list_filters_and_sort(client, backfilled):89    r = client.get("/v1/futures/ES/contracts")90    assert r.status_code == 20091    rows = r.json()["data"]92    exps = [x["expiration_date"] for x in rows]93    assert exps == sorted(exps) and len(rows) >= 1494    assert rows[0]["expiration_source"] == "rule" and rows[0]["timeframes"]["1day"]["rows"] > 095    assert rows[0]["files"]["1day"][0].endswith("_1day.parquet")96    r = client.get("/v1/futures/es/contracts?status=active&sort=-expiration_date")97    act = r.json()["data"]98    assert {x["status"] for x in act} == {"active"} and act[0]["expiration_date"] > act[-1]["expiration_date"]99    assert {x["symbol"] for x in act} == {"ESU25", "ESZ25", "ESH26", "ESM26"}100    r = client.get("/v1/futures/ES/contracts?from=2024-01-01&to=2024-12-31")101    assert {x["symbol"] for x in r.json()["data"]} == {"ESH24", "ESM24", "ESU24", "ESZ24"}102    r = client.get("/v1/futures/6E/contracts")103    assert r.status_code == 200 and r.json()["meta"]["root"] == "E6" and r.json()["meta"]["count"] >= 8104    assert client.get("/v1/futures/ES/contracts?sort=bogus").json()["error"]["code"] == "INVALID_PARAMETER"105    assert client.get("/v1/futures/ES/contracts?status=maybe").json()["error"]["code"] == "INVALID_PARAMETER"106    r = client.get("/v1/futures/ES/contracts?format=csv")107    assert r.status_code == 200 and r.headers["X-Row-Count"] == str(len(rows))108109110# ---- bars ------------------------------------------------------------------------------------------------------111112def test_contract_bars_daily_json(client, backfilled):113    r = client.get("/v1/futures/contract/ESH25/bars?interval=1d&from=2025-01-01&to=2025-01-10")114    assert r.status_code == 200115    body = r.json()116    assert body["meta"]["symbol"] == "ESH25" and body["meta"]["interval"] == "1d" and body["meta"]["next_cursor"] is None117    assert body["meta"]["expiration_date"] == "2025-03-21" and body["meta"]["expiration_source"] == "rule"118    dates = [x["datetime"] for x in body["data"]]119    assert dates == sorted(dates) and dates[0] == "2025-01-01" and dates[-1] == "2025-01-10"   # fixture = plain weekdays120    assert set(body["data"][0]) == {"symbol", "datetime", "open", "high", "low", "close", "volume", "open_interest"}121    assert r.headers["X-Row-Count"] == str(len(dates))122123124def test_contract_bars_intraday_utc_and_sessions(client, backfilled):125    # November 2024 (EST, UTC-5): RTH starts 09:30 ET = 14:30Z; ETH block 08:00 ET = 13:00Z126    r = client.get("/v1/futures/contract/es_h25/bars?interval=1m&session=rth&limit=2")127    d = r.json()["data"]128    assert d[0]["datetime"].endswith("T14:30:00Z") and d[1]["datetime"].endswith("T14:31:00Z")129    assert "open_interest" not in d[0]130    r = client.get("/v1/futures/contract/ESH2025/bars?interval=1m&session=eth&limit=1")131    assert r.json()["data"][0]["datetime"].endswith("T13:00:00Z")132    r = client.get("/v1/futures/contract/ES_H25/bars?interval=1m&session=all&limit=1")133    assert r.json()["data"][0]["datetime"].endswith("T13:00:00Z")134    # June 2025 (EDT, UTC-4): 09:30 ET = 13:30Z — DST handled135    r = client.get("/v1/futures/contract/ESU25/bars?interval=1m&session=rth&from=2025-06-16&limit=1")136    assert r.json()["data"][0]["datetime"] == "2025-06-16T13:30:00Z"137    # timeframe alias and 1hour interval138    r = client.get("/v1/futures/contract/ESU25/bars?timeframe=1hour&limit=1&from=2025-06-16")139    assert r.status_code == 200 and r.json()["meta"]["interval"] == "1h"140    # FX root uses its own RTH window (08:20 ET → 13:20Z in December)141    r = client.get("/v1/futures/contract/6EZ25/bars?interval=1m&session=rth&limit=1")142    assert r.json()["meta"]["symbol"] == "E6Z25" and r.json()["data"][0]["datetime"].endswith("T13:20:00Z")143144145def test_contract_bars_from_to_datetime_bounds(client, backfilled):146    r = client.get("/v1/futures/contract/ESU25/bars?interval=1m&from=2025-06-16T13:30:00Z&to=2025-06-16T13:35:00Z")147    d = r.json()["data"]148    assert [x["datetime"] for x in d] == [f"2025-06-16T13:3{i}:00Z" for i in range(5)]149    r = client.get("/v1/futures/contract/ESU25/bars?interval=1m&from=not-a-date")150    assert r.status_code == 400 and r.json()["error"]["code"] == "INVALID_PARAMETER"151152153def test_cursor_pagination(client_hu, backfilled):154    r1 = client_hu.get("/v1/futures/contract/ESH25/bars?interval=5m&limit=7")155    b1 = r1.json()156    assert len(b1["data"]) == 7 and b1["meta"]["next_cursor"]157    r2 = client_hu.get(f"/v1/futures/contract/ESH25/bars?interval=5m&limit=7&cursor={b1['meta']['next_cursor']}")158    b2 = r2.json()159    assert len(b2["data"]) == 7 and b2["data"][0]["datetime"] > b1["data"][-1]["datetime"]160    full = client_hu.get("/v1/futures/contract/ESH25/bars?interval=5m&limit=14").json()["data"]161    assert [x["datetime"] for x in b1["data"] + b2["data"]] == [x["datetime"] for x in full]162    last = client_hu.get("/v1/futures/contract/ESH25/bars?interval=1d&limit=100000").json()163    assert last["meta"]["next_cursor"] is None164    assert client_hu.get("/v1/futures/contract/ESH25/bars?cursor=%%%").json()["error"]["code"] == "INVALID_PARAMETER"165    assert client_hu.get("/v1/futures/contract/ESH25/bars?limit=0").json()["error"]["code"] == "INVALID_PARAMETER"166167168def test_bars_csv_and_parquet(client, backfilled):169    r = client.get("/v1/futures/contract/ESH25/bars?interval=1m&limit=3&format=csv")170    assert r.status_code == 200 and r.headers["content-type"].startswith("text/csv") and r.headers["X-Row-Count"] == "3"171    lines = r.text.strip().splitlines()172    assert lines[0] == "symbol,datetime,open,high,low,close,volume" and lines[1].split(",")[1].endswith("Z")173    r = client.get("/v1/futures/contract/ESH25/bars?interval=1d&limit=2&format=csv")174    assert r.text.splitlines()[1].split(",")[1] == "2024-02-15"       # daily = plain date175    r = client.get("/v1/futures/contract/ESH25/bars?interval=1m&limit=50&format=parquet")176    assert r.status_code == 200 and r.headers["content-type"] == "application/vnd.apache.parquet" and r.headers["X-Row-Count"] == "50"177    df = pd.read_parquet(io.BytesIO(r.content))178    assert len(df) == 50 and str(df["datetime"].dtype).endswith("UTC]") and list(df.columns)[:2] == ["symbol", "datetime"]179180181def test_bars_merge_archive_and_update_dedup(client_hu, backfilled):182    """ESH25 exists in both buckets (archive ≤ 2024-12-31, update full): no duplicate dates."""183    d = client_hu.get("/v1/futures/contract/ESH25/bars?interval=1d&limit=100000").json()["data"]184    dates = [x["datetime"] for x in d]185    assert len(dates) == len(set(dates)) and dates[0] < "2024-12-31" < dates[-1]186187188# ---- coverage ------------------------------------------------------------------------------------------------189190def test_coverage(client, backfilled):191    r = client.get("/v1/futures/contract/ESH25/coverage")192    assert r.status_code == 200193    c = r.json()["data"]194    assert c["symbol"] == "ESH25" and c["expiration_source"] == "rule" and c["expiration_date"] == "2025-03-21"195    assert c["intervals"]["1d"]["available"] and c["intervals"]["1d"]["open_interest"] is True and c["intervals"]["1d"]["rows"] > 200196    assert c["intervals"]["1m"]["available"] and c["intervals"]["1m"]["open_interest"] is False197    assert set(c["intervals"]["1m"]["files"]) == {"archive", "update"}198    assert c["gaps"] == [] and isinstance(c["notes"], list)199200201# ---- chain / term structure ------------------------------------------------------------------------------------202203def test_chain(client, backfilled):204    r = client.get("/v1/futures/ES/chain?as_of=2024-09-10")205    assert r.status_code == 200206    body = r.json()207    rows = body["data"]208    assert body["meta"]["front"] == "ESU24" and rows[0]["position"] == 1 and rows[0]["days_to_expiry"] == 10209    assert [x["symbol"] for x in rows] == ["ESU24", "ESZ24", "ESH25", "ESM25", "ESU25"]210    assert rows[0]["close"] and rows[0]["last_date"] == "2024-09-10" and rows[0]["open_interest"] > 0211    assert all(x["expiration_date"] >= "2024-09-10" for x in rows)212    r = client.get("/v1/futures/ES/chain?as_of=2010-01-01")213    assert r.json()["data"] == [] and r.json()["meta"]["note"]214    assert client.get("/v1/futures/ES/chain?as_of=yesterday").json()["error"]["code"] == "INVALID_PARAMETER"215    assert client.get("/v1/futures/ES/chain?as_of=2024-09-10&format=csv").status_code == 200216217218def test_term_structure(client, backfilled):219    r = client.get("/v1/futures/cl/term-structure?as_of=2025-06-30")220    assert r.status_code == 200221    body = r.json()222    rows, meta = body["data"], body["meta"]223    assert meta["root"] == "CL" and meta["structure"] in ("contango", "backwardation", "flat")224    assert rows[0]["slope_annualized"] is None and rows[0]["settle"] == meta["front_settle"]225    assert rows[1]["slope_annualized"] is not None and rows[1]["spread_vs_front"] == pytest.approx(rows[1]["settle"] - rows[0]["settle"])226    dte = [x["days_to_expiry"] for x in rows]227    assert dte == sorted(dte) and dte[0] >= 0228    if meta["structure"] == "contango":229        assert rows[1]["settle"] > rows[0]["settle"]230231232# ---- continuous ------------------------------------------------------------------------------------------------233234def test_continuous_calendar_roll_and_back_adjustment(client, backfilled):235    r = client.get("/v1/futures/ES/continuous?roll=calendar&adjust=none&from=2024-03-01&to=2024-03-31")236    assert r.status_code == 200237    raw = r.json()238    syms = [x["symbol"] for x in raw["data"]]239    assert syms[0] == "ESH24" and syms[-1] == "ESM24" and syms == sorted(syms, key=syms.index)240    assert raw["meta"]["roll_dates"] == [pytest.approx(raw["meta"]["roll_dates"][0])]241    rd = raw["meta"]["roll_dates"][0]242    assert rd["date"] == "2024-03-18" and rd["from_symbol"] == "ESH24" and rd["to_symbol"] == "ESM24" and rd["adjusted"]243    assert rd["gap_session"] == "2024-03-15" and rd["gap"] == pytest.approx(rd["ratio"] * 0 + rd["gap"])244    # switch day: last ESH24 bar is its expiry (Fri 15), first ESM24 bar is Mon 18245    last_h = max(x["datetime"] for x in raw["data"] if x["symbol"] == "ESH24")246    first_m = min(x["datetime"] for x in raw["data"] if x["symbol"] == "ESM24")247    assert last_h == "2024-03-15" and first_m == "2024-03-18"248    adj = client.get("/v1/futures/ES/continuous?roll=calendar&adjust=back_adjusted&from=2024-03-01&to=2024-03-31").json()249    raw_close = {(x["symbol"], x["datetime"]): x["close"] for x in raw["data"]}250    offsets = {}251    for x in adj["data"]:252        offsets.setdefault(x["symbol"], set()).add(round(x["close"] - raw_close[(x["symbol"], x["datetime"])], 6))253    assert len(offsets["ESH24"]) == 1 and len(offsets["ESM24"]) == 1          # constant shift per segment254    assert (offsets["ESH24"].pop() - offsets["ESM24"].pop()) == pytest.approx(rd["gap"], abs=1e-6)255    assert adj["data"][0]["volume"] == raw["data"][0]["volume"]              # volume never adjusted256    # the last segment (latest contract) is unadjusted257    tail = client.get("/v1/futures/ES/continuous?roll=calendar&adjust=back_adjusted&from=2025-06-25").json()258    tail_raw = client.get("/v1/futures/contract/ESU25/bars?interval=1d&from=2025-06-25").json()259    assert tail["meta"]["unadjusted_symbol"] == "ESM26"260    assert {x["symbol"] for x in tail["data"]} == {"ESU25"} or tail["data"][-1]["symbol"] == tail["meta"]["unadjusted_symbol"]261    if {x["symbol"] for x in tail["data"]} == {"ESU25"}:262        assert tail["data"][-1]["close"] != tail_raw["data"][-1]["close"] or tail["meta"]["rolls_total"] > 0263264265def test_continuous_volume_roll_before_expiry_and_ratio(client, backfilled):266    r = client.get("/v1/futures/ES/continuous?roll=volume&adjust=ratio_adjusted&from=2024-03-01&to=2024-03-31").json()267    rd = r["meta"]["roll_dates"]268    assert len(rd) == 1 and rd[0]["from_symbol"] == "ESH24" and rd[0]["to_symbol"] == "ESM24"269    assert "2024-03-08" < rd[0]["date"] < "2024-03-15"            # volume roll a few sessions before expiry270    assert rd[0]["ratio"] and rd[0]["ratio"] != 1.0271    assert r["meta"]["roll"] == "volume" and r["meta"]["adjust"] == "ratio_adjusted"272    oi = client.get("/v1/futures/ES/continuous?roll=open_interest&from=2024-03-01&to=2024-03-31").json()273    assert oi["meta"]["roll_dates"][0]["to_symbol"] == "ESM24" and oi["meta"]["roll_dates"][0]["date"] < "2024-03-18"274275276def test_continuous_first_notice_roll(client, backfilled):277    r = client.get("/v1/futures/CL/continuous?roll=first_notice&from=2024-03-15&to=2024-03-25").json()278    rd = r["meta"]["roll_dates"]279    assert len(rd) == 1 and rd[0]["date"] == "2024-03-21" and rd[0]["from_symbol"] == "CLJ24" and rd[0]["to_symbol"] == "CLN24"280281282def test_continuous_depth(client, backfilled):283    r1 = client.get("/v1/futures/ES/continuous?roll=calendar&from=2024-03-01&to=2024-03-31").json()284    r2 = client.get("/v1/futures/ES/continuous?roll=calendar&depth=2&from=2024-03-01&to=2024-03-31").json()285    r3 = client.get("/v1/futures/ES/continuous?roll=calendar&depth=3&from=2024-03-01&to=2024-03-31").json()286    assert [x["symbol"] for x in r2["data"]][0] == "ESM24" and r2["data"][-1]["symbol"] == "ESU24"287    assert r3["data"][0]["symbol"] == "ESU24" and r3["data"][-1]["symbol"] == "ESZ24"288    assert len(r1["data"]) == len(r2["data"]) == len(r3["data"])289    assert r2["meta"]["roll_dates"][0]["from_symbol"] == "ESM24" and r2["meta"]["roll_dates"][0]["to_symbol"] == "ESU24"290    assert client.get("/v1/futures/ES/continuous?depth=4").json()["error"]["code"] == "INVALID_PARAMETER"291292293def test_continuous_intraday_applies_daily_schedule(client_hu, backfilled):294    r = client_hu.get("/v1/futures/ES/continuous?interval=1m&roll=volume&from=2025-06-10&to=2025-06-25&session=rth&limit=100000")295    assert r.status_code == 200296    body = r.json()297    syms = [x["symbol"] for x in body["data"]]298    assert set(syms) == {"ESM25", "ESU25"} and syms == sorted(syms, key=syms.index)299    ts = [x["datetime"] for x in body["data"]]300    assert ts == sorted(ts) and all(t.endswith("Z") for t in ts) and ts[0] >= "2025-06-10T13:30:00Z"301    assert len(body["meta"]["roll_dates"]) == 1 and body["meta"]["roll_dates"][0]["to_symbol"] == "ESU25"302    assert "open_interest" not in body["data"][0]303    page = client_hu.get("/v1/futures/ES/continuous?interval=1m&roll=volume&from=2025-06-10&to=2025-06-25&session=rth&limit=100").json()304    nxt = client_hu.get(f"/v1/futures/ES/continuous?interval=1m&roll=volume&from=2025-06-10&to=2025-06-25&session=rth&limit=100&cursor={page['meta']['next_cursor']}").json()305    assert nxt["data"][0]["datetime"] > page["data"][-1]["datetime"]306    r = client_hu.get("/v1/futures/ES/continuous?interval=1m&from=2025-06-10&to=2025-06-12&format=parquet")307    assert r.status_code == 200 and int(r.headers["X-Row-Count"]) > 0308    r = client_hu.get("/v1/futures/ES/continuous?from=2025-06-10&to=2025-06-12&format=csv")309    assert r.text.splitlines()[0].startswith("symbol,datetime")310311312def test_continuous_alias_root_and_empty_window(client, backfilled):313    r = client.get("/v1/futures/6E/continuous?roll=calendar&from=2025-06-20&to=2025-06-30")314    assert r.status_code == 200 and r.json()["meta"]["root"] == "E6" and r.json()["data"]315    r = client.get("/v1/futures/ES/continuous?from=2010-01-01&to=2010-01-31")316    assert r.status_code == 200 and r.json()["data"] == [] and r.json()["meta"]["roll_dates"] == []317318319# ---- errors ------------------------------------------------------------------------------------------------------320321@pytest.mark.parametrize("url, status, code", [322    ("/v1/futures/contract/BOGUS/bars", 400, "INVALID_CONTRACT_SYMBOL"),323    ("/v1/futures/contract/ESZ5/coverage", 400, "INVALID_CONTRACT_SYMBOL"),324    ("/v1/futures/contract/ESZ99/bars", 404, "CONTRACT_NOT_FOUND"),325    ("/v1/futures/contract/ESZ99/coverage", 404, "CONTRACT_NOT_FOUND"),326    ("/v1/futures/NOPE/chain", 404, "ROOT_NOT_FOUND"),327    ("/v1/futures/NOPE/contracts", 404, "ROOT_NOT_FOUND"),328    ("/v1/futures/NOPE/continuous", 404, "ROOT_NOT_FOUND"),329    ("/v1/futures/NOPE/term-structure", 404, "ROOT_NOT_FOUND"),330    ("/v1/futures/ES/continuous?roll=weird", 400, "INVALID_PARAMETER"),331    ("/v1/futures/ES/continuous?adjust=weird", 400, "INVALID_PARAMETER"),332    ("/v1/futures/contract/ESH25/bars?interval=2h", 400, "INVALID_PARAMETER"),333    ("/v1/futures/contract/ESH25/bars?session=lunch", 400, "INVALID_PARAMETER"),334    ("/v1/futures/contract/ESH25/bars?format=xml", 400, "INVALID_PARAMETER"),335])336def test_error_envelopes(client, backfilled, url, status, code):337    r = client.get(url)338    assert r.status_code == status339    body = r.json()340    assert body["error"]["code"] == code and body["error"]["docs"].endswith(f"#{code.lower()}") and body["detail"]341342343def test_tier_row_cap_is_enforced_via_request_state(client, backfilled, app):344    """clamp_limit(..., request=request) honours request.state.max_rows set by a limiter middleware."""345    from starlette.middleware.base import BaseHTTPMiddleware346347    class Cap(BaseHTTPMiddleware):348        async def dispatch(self, request, call_next):349            request.state.max_rows = 10350            return await call_next(request)351    # build a throwaway app sharing the routes352    from core import errors353    from fastapi import FastAPI354    from fastapi.testclient import TestClient355    from futures.routes import router356    a = FastAPI()357    errors.install(a)358    a.add_middleware(Cap)359    a.include_router(router)360    with TestClient(a) as c:361        r = c.get("/v1/futures/contract/ESH25/bars?limit=50")362        assert r.status_code == 400 and r.json()["error"]["code"] == "ROW_LIMIT_EXCEEDED" and r.json()["error"]["details"]["max_rows"] == 10363        r = c.get("/v1/futures/contract/ESH25/bars?limit=5")364        assert r.status_code == 200 and r.headers["X-Row-Count"] == "5"365366367# ---- OpenAPI -----------------------------------------------------------------------------------------------------368369def test_openapi_documents_the_seven_endpoints(client, backfilled):370    spec = client.get("/openapi.json").json()371    paths = spec["paths"]372    expected = ["/v1/futures/roots", "/v1/futures/{root}/contracts", "/v1/futures/contract/{symbol}/bars",373                "/v1/futures/contract/{symbol}/coverage", "/v1/futures/{root}/chain", "/v1/futures/{root}/continuous",374                "/v1/futures/{root}/term-structure"]375    for p in expected:376        op = paths[p]["get"]377        assert op["tags"] == ["futures"] and op["summary"] and len(op["description"]) > 80378        assert "200" in op["responses"] and "429" in op["responses"]379    bars = paths["/v1/futures/contract/{symbol}/bars"]["get"]380    assert "CONTRACT_NOT_FOUND" in bars["responses"]["404"]["description"]381    assert "INVALID_CONTRACT_SYMBOL" in bars["responses"]["400"]["description"]382    assert "example" in bars["responses"]["200"]["content"]["application/json"]383    names = {p["name"] for p in bars["parameters"]}384    assert {"interval", "from", "to", "session", "cursor", "limit", "format"} <= names385