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)
JavaScript 53.7%
Python 38.3%
CSS 4.6%
TypeScript 3.1%
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