Python SDK
The fdc package wraps the REST API, the WebSocket stream and bulk file downloads behind one client. Every method returns the same normalized rows, whichever transport delivers them.
This reference covers fdc 0.9, which targets API v1 (draft) and schema 0.9. If you have not made a request yet, start with the quickstart.
Install
pip install fdc # core: iterators of dicts, Arrow tables, downloads
pip install "fdc[pandas]" # adds .to_pandas()
pip install "fdc[polars]" # adds .to_polars()
pip install "fdc[all]" # bothPython 3.9 or later on Linux, macOS and Windows. The core package depends on httpx, websockets and pyarrow.
Client
from fdc import Client
fdc = Client(
api_key="fdc_live_...", # or leave out and set FDC_API_KEY
timeout=30.0,
region="ap-northeast-1",
retries=3,
)| Parameter | Type | Default | Description |
|---|---|---|---|
api_key | str | None | None | Your key. When omitted, read from the FDC_API_KEY environment variable. A fdc_test_ key returns the sample day. See Authentication. |
timeout | float | 30.0 | Seconds to wait for each HTTP response and for the WebSocket handshake. |
region | str | "auto" | Stream edge for live data: "ap-northeast-1" (Tokyo), "eu-west-1" (Ireland) or "auto" (lowest round trip at connect time). REST is routed globally and is unaffected. |
retries | int | 3 | Retries for HTTP 429, 5xx and connection errors, with exponential backoff and jitter. Retry-After is honored. 0 disables retries. |
base_url | str | None | None | Override the REST base URL, for proxies. |
A client holds a connection pool and is safe to share between threads. Use it as a context manager, or call fdc.close(), to release connections and stop open streams.
Common arguments
Every data method accepts these keyword arguments. Method-specific arguments are listed under each method.
| Argument | Type | Default | Description |
|---|---|---|---|
venue | str | One venue id, for example "hyperliquid". Use either venue or venues. | |
venues | list[str] | Several venue ids, returned as one stream ordered on ts_capture. | |
symbol | str | list[str] | Normalized symbol or list, for example "BTC-PERP". See Symbology. | |
start | str | int | datetime | required | Inclusive start: ISO 8601 string, date string (00:00 UTC), int64 nanoseconds, or timezone-aware datetime. |
end | same as start | None | Exclusive end. Required unless live=True. |
live | bool | False | Backfill from start, then continue on the live stream. Needs the live scope. See Live mode. |
clock | str | "capture" | Clock for start/end: "capture" or "exchange". |
fields | list[str] | None | None | Return only these columns. Common columns are always included. |
revision | int | None | None | Read a specific data revision instead of the current one. See Data quality. |
limit | int | None | None | Stop after this many rows. |
Methods
Data methods return a Stream (see Return types). Nothing is fetched until you iterate it or convert it.
trades, quotes
source filters trades to "fdc_capture" or "venue_archive". origin filters quotes to "venue" or "book".
candles
interval is "1s", "1m", "5m", "1h" or "1d". With live=True a candle is yielded when its bucket closes.
book
| Argument | Type | Default | Description |
|---|---|---|---|
depth | str | "L2" | "L2", "L3" or "L4". L3 and L4 need the Desk plan or above. |
levels | int | None | None | L2 only: truncate to the top N levels per side. |
on_gap | str | callable | "raise" | "raise" raises fdc.GapError when the replay crosses a recorded gap; "resync" skips to the snapshot after the gap; a callable receives the gap record and the replay continues from the next snapshot. |
The first rows are a synthetic snapshot of the book as of start, then deltas (L2) or order events (L3, L4) follow. Semantics are in Order book depth.
funding
kind is "settled", "predicted" or None for both. grid="1h" returns one row per contract per UTC hour with the rate scaled to one hour, so Hyperliquid's hourly funding and Binance's 8-hour funding line up row for row. Omitting symbol returns every perpetual on the selected venues.
open_interest, liquidations, mark_index
extended
fields takes field keys ("ofi_ml", "vpin", "queue", "cxl_fill", "resil", "lambda", "markout", "part") or "field:variant" pairs. emit is "grid", "event" or None for both. wide=True returns one column per field and variant. Definitions are in Extended fields.
instruments
Reference data with full history. as_of returns the rows in force at a past time, including instruments since delisted; history=True returns every version.
gaps
Recorded gaps: intervals every capture region missed. reason filters on the gap reason, for example "seq_break". See Data quality.
chain_flows, chain_entities
Need the chain scope. Columns are in Chain intelligence.
usage
Requests, rows and bytes for the current billing month, or for month="2027-03".
download
Writes one file per symbol per UTC day under path, in the same layout as the S3 archive, and returns the file paths. format is "parquet", "csv" (written as .csv.gz) or "json" (.jsonl.gz). Downloads go through the API with your key and need the historical scope; no AWS account is required. Each file is checked against the fdc.sha256 digest before the call returns. Omitting symbol downloads every symbol of the dataset on that venue.
paths = fdc.download(
dataset="liquidations",
venue="hyperliquid",
start="2026-07-01", end="2026-08-01",
path="./fdc-data",
)
# ['./fdc-data/liquidations/venue=hyperliquid/date=2026-07-01/BTC-PERP.parquet', ...]For many months of book data, reading S3 directly is faster.
Return types
A Stream is an iterator of plain dicts, one per row, with the columns in Schema. Decimal columns (prices, sizes, rates, amounts) are decimal.Decimal. Timestamps are int nanoseconds.
| Member | Returns | Description |
|---|---|---|
for row in stream | dict | Iterate rows. Pages are fetched ahead in a background thread. |
to_pandas(decimals="decimal", timestamps="int") | pandas.DataFrame | decimals: "decimal" (object column of Decimal), "float" (float64) or "str". timestamps: "int" or "datetime" (datetime64[ns, UTC]). |
to_polars(decimals="decimal", timestamps="int") | polars.DataFrame | Decimal columns are pl.Decimal(38, 18) by default. |
to_arrow() | pyarrow.Table | Arrow table with the Parquet types. |
phase | str | "backfill" or "live". |
last_seq | dict | Last seq yielded, per (venue, symbol). |
gaps | list[dict] | Gap records met so far, in the format of the gaps table. |
close() | None | Stop fetching and close any WebSocket. Streams are also context managers. |
Converting reads the stream to its end, so to_pandas() and to_polars() need a finite range; calling them on a live=True stream with no end raises ValueError. A stream can be iterated once.
Live mode
With live=True the SDK delivers history and live data as one stream, in sequence order, with each message exactly once. The handover works like this:
start (for books, from the synthetic snapshot at start) up to the seq of the first buffered live message. stream.phase is "backfill".seq are dropped; the rest are yielded in order.stream.phase becomes "live", the optional on_live callback fires once, and messages are yielded as they arrive.The live buffer holds up to 2,000,000 messages (buffer= to change it). If backfill is slow enough to overflow it, the SDK discards the buffer, extends the backfill and resubscribes with resume_from, so no message is lost. On a disconnect the client reconnects and resumes from stream.last_seq; if the outage outlasts the server's replay buffer, the missing range is backfilled over REST first. Your loop keeps running in both cases. If you pass end as well, the stream finishes when ts_capture passes end.
A gap in the live stream is handled by on_gap exactly as in history: with the default the stream raises fdc.GapError, and with "resync" it continues from the venue snapshot that follows the gap.
def went_live(stream):
log.info("caught up at %s", stream.last_seq)
with fdc.book(
venue="hyperliquid", symbol="BTC-PERP", depth="L3",
start="2027-01-15T00:00:00Z", live=True,
on_live=went_live, on_gap="resync",
) as book:
for msg in book:
handle(msg)With several venues, rows are ordered on ts_capture during backfill and yielded in arrival order once live, since sorting would add latency. Each (venue, symbol) stays in seq order in both phases.
Async usage
AsyncClient takes the same constructor arguments and has the same methods. Data methods return an AsyncStream, iterated with async for; conversions are awaited.
import asyncio
from fdc import AsyncClient
async def main(queue):
async with AsyncClient() as fdc:
hourly = await fdc.funding(
venues=["hyperliquid", "binance"], symbol="BTC-PERP",
grid="1h", start="2027-09-01", end="2027-10-01",
).to_polars()
async for t in fdc.trades(
venue="hyperliquid", symbol="SOL-PERP",
start="2027-01-15", live=True,
):
await queue.put(t)
asyncio.run(main(asyncio.Queue()))One AsyncClient can run many streams concurrently on one event loop. Streams on the same client share WebSocket connections, up to 250 subscriptions per connection. Plan limits are on Rate limits.
Exceptions
All exceptions can be imported from fdc and derive from fdc.FdcError, which carries status, code, message and request_id. The codes are the API's error codes; quote the request_id when you contact support.
| Exception | Raised when |
|---|---|
AuthenticationError | HTTP 401 or WebSocket close 4401: missing_api_key, malformed_api_key, invalid_api_key, revoked_api_key, expired_api_key. |
PermissionDeniedError | HTTP 403 or WebSocket close 4403: insufficient_scope, plan_required, test_key_out_of_range, ip_not_allowed, account_suspended. |
InvalidRequestError | HTTP 400: bad argument, end before start, too many symbols. |
NotFoundError | HTTP 404: unknown venue or symbol, or a venue not live yet (venue_not_available). |
RateLimitError | HTTP 429 after retries are exhausted. retry_after holds seconds to wait. |
ServerError | HTTP 5xx after retries are exhausted. |
FdcTimeoutError | No response within timeout after retries. |
StreamError | The WebSocket closed with a code the client does not recover from on its own. close_code holds it. |
GapError | A replay or live stream crossed a recorded gap with on_gap="raise". gap holds the record. |
import time
import fdc as fdclib
try:
df = fdc.book(venue="hyperliquid", symbol="BTC-PERP", depth="L4",
start="2026-09-15", end="2026-09-15T01:00:00Z").to_polars()
except fdclib.PermissionDeniedError as e:
print(e.code, e.request_id) # plan_required req_01JA7Q3V8K2M...
except fdclib.RateLimitError as e:
time.sleep(e.retry_after)Logging
The SDK logs through the standard logging module under the fdc logger and installs no handlers. At INFO it logs phase changes, reconnects and gaps. At DEBUG it logs each request with URL, status, duration and request_id, and every WebSocket control frame. Keys are always masked to the prefix and last four characters (fdc_live_…3dFu).
import logging
logging.basicConfig(level=logging.INFO)
logging.getLogger("fdc").setLevel(logging.DEBUG)
# or, without code changes: FDC_LOG=debug python script.pyVersions and compatibility
- The SDK follows semantic versioning. Until 1.0, a minor release (0.9 to 0.10) may change behavior; patch releases do not. Pin with
fdc~=0.9.0in production. - Each SDK minor version matches a schema version.
fdc0.9.x sendsFDC-Schema-Version: 0.9on every request and checks the version on every response. - Breaking schema changes are announced at least 90 days ahead, and the old and new versions are served in parallel for at least 12 months. The SDK logs a warning when your pinned version has a scheduled end date.
- Optional dependencies: pandas 1.5 or later, polars 0.20 or later.
Release notes for the SDK, the APIs and the schema are kept together in the changelog.