DOCS/ACCESS

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

SHELL
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]"          # both

Python 3.9 or later on Linux, macOS and Windows. The core package depends on httpx, websockets and pyarrow.

Client

PYTHON
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,
)
ParameterTypeDefaultDescription
api_keystr | NoneNoneYour key. When omitted, read from the FDC_API_KEY environment variable. A fdc_test_ key returns the sample day. See Authentication.
timeoutfloat30.0Seconds to wait for each HTTP response and for the WebSocket handshake.
regionstr"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.
retriesint3Retries for HTTP 429, 5xx and connection errors, with exponential backoff and jitter. Retry-After is honored. 0 disables retries.
base_urlstr | NoneNoneOverride 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.

ArgumentTypeDefaultDescription
venuestrOne venue id, for example "hyperliquid". Use either venue or venues.
venueslist[str]Several venue ids, returned as one stream ordered on ts_capture.
symbolstr | list[str]Normalized symbol or list, for example "BTC-PERP". See Symbology.
startstr | int | datetimerequiredInclusive start: ISO 8601 string, date string (00:00 UTC), int64 nanoseconds, or timezone-aware datetime.
endsame as startNoneExclusive end. Required unless live=True.
liveboolFalseBackfill from start, then continue on the live stream. Needs the live scope. See Live mode.
clockstr"capture"Clock for start/end: "capture" or "exchange".
fieldslist[str] | NoneNoneReturn only these columns. Common columns are always included.
revisionint | NoneNoneRead a specific data revision instead of the current one. See Data quality.
limitint | NoneNoneStop 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

fdc.trades(*, venue=None, venues=None, symbol, start, end=None, live=False, clock="capture", source=None, fields=None, revision=None, limit=None) -> Stream fdc.quotes(*, venue=None, venues=None, symbol, start, end=None, live=False, clock="capture", origin=None, fields=None, revision=None, limit=None) -> Stream

source filters trades to "fdc_capture" or "venue_archive". origin filters quotes to "venue" or "book".

candles

fdc.candles(*, venue=None, venues=None, symbol, interval="1m", start, end=None, live=False, fields=None, revision=None, limit=None) -> Stream

interval is "1s", "1m", "5m", "1h" or "1d". With live=True a candle is yielded when its bucket closes.

book

fdc.book(*, venue=None, venues=None, symbol, depth="L2", start, end=None, live=False, levels=None, on_gap="raise", clock="capture", fields=None, revision=None, limit=None) -> Stream
ArgumentTypeDefaultDescription
depthstr"L2""L2", "L3" or "L4". L3 and L4 need the Desk plan or above.
levelsint | NoneNoneL2 only: truncate to the top N levels per side.
on_gapstr | 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

fdc.funding(*, venue=None, venues=None, symbol=None, start, end=None, live=False, kind=None, grid=None, fields=None, revision=None, limit=None) -> Stream

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

fdc.open_interest(*, venue=None, venues=None, symbol=None, start, end=None, live=False, fields=None, revision=None, limit=None) -> Stream fdc.liquidations(*, venue=None, venues=None, symbol=None, start, end=None, live=False, fields=None, revision=None, limit=None) -> Stream fdc.mark_index(*, venue=None, venues=None, symbol, start, end=None, live=False, fields=None, revision=None, limit=None) -> Stream

extended

fdc.extended(*, venue=None, venues=None, symbol, start, end=None, live=False, fields=None, emit=None, wide=False, profile=None, revision=None, limit=None) -> Stream

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

fdc.instruments(*, venue=None, venues=None, symbol=None, venue_symbol=None, base=None, instrument_type=None, as_of=None, history=False) -> Stream

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

fdc.gaps(*, venue, symbol=None, dataset=None, reason=None, start, end) -> Stream

Recorded gaps: intervals every capture region missed. reason filters on the gap reason, for example "seq_break". See Data quality.

chain_flows, chain_entities

fdc.chain_flows(*, entity_id=None, chain=None, venue=None, direction=None, link_type=None, min_confidence=0.5, start, end=None, live=False, limit=None) -> Stream fdc.chain_entities(*, address=None, entity_id=None, chain=None, as_of=None, history=False, min_confidence=0.5) -> Stream

Need the chain scope. Columns are in Chain intelligence.

usage

fdc.usage(month=None) -> dict

Requests, rows and bytes for the current billing month, or for month="2027-03".

download

fdc.download(*, dataset, venue, symbol=None, start, end, format="parquet", path=".", revision=None, workers=8, overwrite=False) -> list[str]

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.

PYTHON
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.

MemberReturnsDescription
for row in streamdictIterate rows. Pages are fetched ahead in a background thread.
to_pandas(decimals="decimal", timestamps="int")pandas.DataFramedecimals: "decimal" (object column of Decimal), "float" (float64) or "str". timestamps: "int" or "datetime" (datetime64[ns, UTC]).
to_polars(decimals="decimal", timestamps="int")polars.DataFrameDecimal columns are pl.Decimal(38, 18) by default.
to_arrow()pyarrow.TableArrow table with the Parquet types.
phasestr"backfill" or "live".
last_seqdictLast seq yielded, per (venue, symbol).
gapslist[dict]Gap records met so far, in the format of the gaps table.
close()NoneStop 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:

01Subscribe firstThe client opens the WebSocket and subscribes before fetching any history. Live messages are buffered from this point.
02BackfillHistory is paged from start (for books, from the synthetic snapshot at start) up to the seq of the first buffered live message. stream.phase is "backfill".
03Splice on seqBuffered messages at or below the last historical seq are dropped; the rest are yielded in order.
04Livestream.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.

PYTHON
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.

PYTHON
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.

ExceptionRaised when
AuthenticationErrorHTTP 401 or WebSocket close 4401: missing_api_key, malformed_api_key, invalid_api_key, revoked_api_key, expired_api_key.
PermissionDeniedErrorHTTP 403 or WebSocket close 4403: insufficient_scope, plan_required, test_key_out_of_range, ip_not_allowed, account_suspended.
InvalidRequestErrorHTTP 400: bad argument, end before start, too many symbols.
NotFoundErrorHTTP 404: unknown venue or symbol, or a venue not live yet (venue_not_available).
RateLimitErrorHTTP 429 after retries are exhausted. retry_after holds seconds to wait.
ServerErrorHTTP 5xx after retries are exhausted.
FdcTimeoutErrorNo response within timeout after retries.
StreamErrorThe WebSocket closed with a code the client does not recover from on its own. close_code holds it.
GapErrorA replay or live stream crossed a recorded gap with on_gap="raise". gap holds the record.
PYTHON
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).

PYTHON
import logging
logging.basicConfig(level=logging.INFO)
logging.getLogger("fdc").setLevel(logging.DEBUG)

# or, without code changes:  FDC_LOG=debug python script.py

Versions 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.0 in production.
  • Each SDK minor version matches a schema version. fdc 0.9.x sends FDC-Schema-Version: 0.9 on 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.