DOCS/EXTENDED DATA

Extended fields

The extended table holds eight derived microstructure fields computed from our own trades, quotes and order books. This page defines every field and variant: the formula, the window, the unit and when a value is emitted.

For an overview of what each field is for, see Extended fields. The out-of-sample test behind the homepage hit-rate figure is written up in our research note on predictive power. This reference describes schema 0.9 (draft), which becomes 1.0 with Phase 1.

Table layout

extended is a long table: one row per field, variant and timestamp. A field is one of eight keys (ofi_ml, vpin, queue, cxl_fill, resil, lambda, markout, part). A variant is one output of that field: field = "ofi_ml" with variant = "l5" is order flow imbalance summed over the top five levels.

ColumnTypeDescription
common columnsvenue, symbol, venue_symbol, ts_exchange, ts_capture, seq, source, region, as on Schema. source is always fdc_derived.
fieldenumField key.
variantstringOutput of the field, listed per field below.
valuefloat64The value, in the unit listed per variant. Null during warm-up, or when the definition has no value (a ratio with a zero denominator, for example).
on_gridbooltrue for rows on the 100 ms grid, false for event rows.
emitenumgrid or event. The same information as on_grid, as a string for readable filters.
ts_availableint64When the value became knowable, in nanoseconds on the ts_capture clock. Equal to ts_capture except on per-trade markout events (trade time plus 60 s) and sweep events (time the measurement closed).
ref_idstringOn per-trade markout events, the trade_id. Null otherwise.

On grid rows, ts_capture is the grid boundary, always a multiple of 100,000,000 ns, and ts_exchange and seq belong to the last input message used. On event rows, ts_capture is the capture time of the message that triggered the event.

The SDK and REST can also return a wide layout with one column per field and variant: wide=True in the SDK, layout=wide in REST. Wide column names are listed in the tables below and match the names on the product page.

One clock, as-of safe

Every input is ordered by ts_capture before any computation, so a field that mixes trades and book updates never mixes two clocks. See Timestamps and clocks.

  • Grid rows (emit = "grid") are stamped at each 100 ms boundary T and built only from messages with ts_capture < T. They can be joined as-of on ts_capture with no lookahead.
  • Event rows (emit = "event") are written when something happens that a grid would blur: a VPIN bucket closes, a sweep is measured, the touch price changes, or a trade's markout horizon closes. Join them on ts_available.

Warm-up, gaps and nulls

A rolling value is null until its window has filled once. Windows fill from the start of capture and again after every recorded gap: when an input stream has a gap, each window that would span it is reset. We never interpolate across a gap. Gap intervals are in /v1/gaps; see Data quality.

A field whose inputs a venue does not publish produces no rows for that venue. queue and cxl_fill need L3; part needs L4 with participant ids. The venue reference lists what each venue publishes.

OFI-ML: multi-level order flow imbalance

Net pressure on the book across the top 10 levels, from book_l2. Size added on the bid or removed from the ask counts as buying pressure; the reverse counts as selling pressure. Levels are indexed by rank from the touch.

# for level m, between book updates n-1 and n e(n,m) = qb(n,m) · 1[Pb(n,m) ≥ Pb(n-1,m)] − qb(n-1,m) · 1[Pb(n,m) ≤ Pb(n-1,m)] − qa(n,m) · 1[Pa(n,m) ≤ Pa(n-1,m)] + qa(n-1,m) · 1[Pa(n,m) ≥ Pa(n-1,m)] OFI_m(t) = Σ e(n,m) over updates in the 100 ms interval l1 = OFI_1 l5 = Σ OFI_m for m = 1..5 l10 = Σ OFI_m for m = 1..10 norm = l10 / mean depth of levels 1..10 (bid + ask) over the last 60 s
VariantWide columnUnitEmit
l1ofi_ml_l1base assetgrid
l5ofi_ml_l5base assetgrid
l10ofi_ml_l10base assetgrid
normofi_ml_normunitlessgrid

VPIN: flow toxicity

The share of recent volume that was one-sided, measured in equal-volume buckets, from trades. Every trade carries its aggressor side, so buckets use actual buy and sell volume and no bulk volume classification is needed. A trade that crosses a bucket boundary is split between the two buckets.

V = trailing 30-day average daily volume / 50 # bucket size, base units VB(τ), VS(τ) = aggressor buy and sell volume in bucket τ value = Σ |VB(τ) − VS(τ)| / (50 · V) over the last 50 buckets cdf = percentile of value over the trailing 30 days

The bucket size is fixed at 00:00 UTC for the whole day, from the 30 complete days before it.

VariantWide columnUnitEmit
valuevpin0 to 1event on each closed bucket; last value carried on the grid
cdfvpin_cdf0 to 1same
bucket_sizevpin_bucket_sizebase assetsame
bucket_seqvpin_bucket_seqbuckets closed since 00:00 UTCsame

QUEUE: queue dynamics

The queue at the best bid and ask, rebuilt order by order from book_l3 under each venue's priority rules. The rebuild for Hyperliquid is described in Reconstructing L3 queues on Hyperliquid.

bid_size, bid_count = resting size and order count at the best bid (ask likewise) age_p50_ms = median age of orders resting at the touch life_p50_ms = median lifetime (add → cancel or full fill), trailing 1 h fill_1s_bid = joins at best bid filled within 1 s / joins at best bid, trailing 1 h

A join counts as filled if it receives any fill within 1,000 ms. Joins less than 1 s old at the row time are left out, since their outcome is not yet known.

VariantWide columnUnitEmit
bid_size, ask_sizequeue_bid_size, queue_ask_sizebase assetgrid, plus event when the touch price changes
bid_count, ask_countqueue_bid_count, queue_ask_countorderssame
age_p50_msqueue_age_p50_msmssame
life_p50_msqueue_life_p50_msmsgrid
fill_1s_bid, fill_1s_askqueue_fill_1s_bid, queue_fill_1s_askprobabilitygrid

CXL/FILL: cancel-to-fill ratio

How much displayed size is pulled before anyone trades against it, from book_l3, over a trailing 10-second window. Size reductions by modify count as cancelled size.

l1 = Σ cancelled size at level 1 / Σ filled size at level 1 l5 = Σ cancelled size in levels 1..5 / Σ filled size in levels 1..5 # trailing 10 s window; null when no fills occurred (never infinite)
VariantWide columnUnitEmit
l1cxl_fill_l1ratio of sizesgrid
l5cxl_fill_l5ratio of sizesgrid
cxl_count_10scxl_count_10scancel eventsgrid
fill_count_10sfill_count_10sfill eventsgrid

RESIL: book resiliency

How fast depth comes back after it is taken, from the top five levels of book_l2 and from trades. A sweep is any 100 ms interval in which aggressive volume removes at least half of the top-five depth on one side.

D(t) = resting size in levels 1..5 on the swept side sweep at ts : D(ts) ≤ 0.5 · D(ts−) refill_ms = first t − ts with D(t) ≥ 0.9 · D(ts−) refill_1s = D(ts + 1 s) / D(ts−) sweep_size = D(ts−) − D(ts) med_5m = median refill_ms over sweeps in the last 5 min

A sweep event is written when depth reaches 90% of its pre-sweep level, or after 60 s if it never does, in which case refill_ms is null. On the event, ts_capture is the sweep time and ts_available is when the measurement closed.

VariantWide columnUnitEmit
refill_msresil_refill_msmsevent per sweep
refill_1sresil_refill_1sratioevent per sweep
sweep_sizeresil_sweep_sizebase assetevent per sweep
med_5mresil_med_5mmsgrid

LAMBDA: price impact

A rolling Kyle-style estimate of how far the mid moves per unit of signed notional, from trades with the mid from quotes. One-second mid returns are regressed on one-second signed notional over the last five minutes.

# k = 1-second buckets on ts_capture, trailing 300 buckets r(k) = (mid(k) − mid(k−1)) / mid(k−1) × 10,000 # bps Q(k) = Σ side · price · size in bucket k, USD millions r(k) = λ · Q(k) + ε(k) # OLS, no intercept bps_per_musd = λ, r2 = R², n = buckets with trades

Buckets without trades enter the fit with Q = 0. The estimate is null when fewer than 30 buckets contain trades. For inverse contracts, notional is the contract value in USD.

VariantWide columnUnitEmit
bps_per_musdlambda_bps_per_musdbps per USD 1M signed notionalre-estimated every 1 s, carried on the grid
r2lambda_r20 to 1same
nlambda_nbucketssame

MARKOUT: post-trade drift

Where the mid went after each trade, at five horizons, split by aggressor side, from trades with the mid from quotes. Positive values mean the price moved the way the aggressor traded.

s = +1 for buy, −1 for sell markout(h) = s · (mid(t + h) − mid(t)) / mid(t) × 10,000 # bps h ∈ {100ms, 1s, 5s, 30s, 60s} # grid row at T: volume-weighted over trades with t + h ≤ T and t > T − h − 60 s # per-trade event row: ts_available = t + 60 s

A markout is knowable only after its horizon has passed. Grid rows include only trades whose horizon had elapsed by the row time, so they are as-of safe like every other field. Per-trade event rows are written when the 60 s horizon closes and carry all five horizons for that trade, with ref_id set to its trade_id. Their ts_capture is the trade's capture time, which helps with grouping; join them on ts_available, never on the trade time.

VariantWide columnUnitEmit
buy_100ms, sell_100msmarkout_buy_100ms, markout_sell_100msbpsgrid, plus per-trade event
buy_1s, sell_1smarkout_buy_1s, markout_sell_1sbpssame
buy_5s, sell_5smarkout_buy_5s, markout_sell_5sbpssame
buy_30s, sell_30smarkout_buy_30s, markout_sell_30sbpssame
buy_60s, sell_60smarkout_buy_60s, markout_sell_60sbpssame

A per-trade event row carries only the variants for that trade's side.

PART: participant flow

Aggregated behavior of attributed participants, from book_l4, on venues that publish who is behind each order. On Hyperliquid the participant id is the trader's on-chain address. This field reports concentration and direction only; per-participant rows stay in book_l4.

# trailing 1-minute window; top 10 ranked by volume over the trailing 1 h active = distinct participant_id with a fill top10_share = top-10 volume / total volume top10_net_flow = Σ side · size for top-10 taker fills maker_hhi = Σ (maker share of each participant)² new_share = volume from ids first seen in the last 24 h / total
VariantWide columnUnitEmit
activepart_activecountgrid
top10_sharepart_top10_sharesharegrid
top10_net_flowpart_top10_net_flowbase assetgrid
maker_hhipart_maker_hhi0 to 1grid
new_sharepart_new_sharesharegrid

Following the same entity across venues is part of chain intelligence, not this field.

Requesting extended fields

Extended fields are included from the Researcher plan, for the venues and instruments the plan covers. Pass field keys in fields, or field:variant pairs such as ofi_ml:l5 for single variants. Omitting fields returns all eight.

import polars as pl
from fdc import Client

fdc = Client(api_key="fdc_live_...")

ext = fdc.extended(
    venue="hyperliquid",
    symbol="BTC-PERP",
    fields=["ofi_ml", "vpin", "markout"],
    start="2026-09-15",
    end="2026-09-16",
).to_polars()

grid = ext.filter(pl.col("emit") == "grid")      # as-of safe on ts_capture
ofi5 = grid.filter((pl.col("field") == "ofi_ml") & (pl.col("variant") == "l5"))

# one column per field and variant instead
wide = fdc.extended(venue="hyperliquid", symbol="BTC-PERP", fields=["ofi_ml"],
                    start="2026-09-15", end="2026-09-16", wide=True).to_polars()
# columns: ..., ofi_ml_l1, ofi_ml_l5, ofi_ml_l10, ofi_ml_norm

Per-trade markouts

Markout event rows are the input for transaction cost analysis. Filter on ts_available so a backtest only sees markouts that had closed by the decision time.

PYTHON
events = ext.filter((pl.col("emit") == "event") & (pl.col("field") == "markout"))

decision_ts = 1789482600000000000              # 2026-09-15 14:30 UTC, ns
known = events.filter(pl.col("ts_available") <= decision_ts)

per_trade = known.pivot(on="variant", index=["ref_id", "ts_capture"], values="value")

Live

On Desk and Enterprise plans the same rows stream on the extended WebSocket channel. The streaming and historical computations share code, and they must match exactly on recorded data before a field is released. With live=True the SDK backfills and continues live from the next grid boundary. See WebSocket and Python SDK.

Custom profiles (Enterprise)

Enterprise accounts can have fields computed with other windows and thresholds. A profile is a frozen parameter set with an id such as acme-fast-v2; changing any parameter creates a new id, so earlier results stay reproducible. Profile rows carry the id in a profile column (null for the default parameters), are requested with profile= in REST and the SDK, and are stored under extended_{profile}/ in S3.

Live computation for a new profile starts within 2 business days. Recomputing the full capture history usually takes up to 5 business days per venue. Send parameter sets to daniel@financedatacloud.com.

ParameterDefaultAllowed range
grid.interval_ms10010 to 1,000
ofi_ml.levels1, 5, 10Up to 4 cut-offs, 1 to 20
ofi_ml.norm_window_s6010 to 900
vpin.buckets5010 to 500
vpin.adv_days305 to 90
queue.fill_horizon_ms1,000100 to 10,000
queue.lookback_s3,60060 to 86,400
cxl_fill.window_s101 to 3,600
resil.levels51 to 20
resil.sweep_share0.50.2 to 0.9
resil.refill_target0.90.5 to 1.0
lambda.bucket_ms1,000100 to 60,000
lambda.window_buckets30060 to 7,200
markout.horizons100ms, 1s, 5s, 30s, 60sUp to 8 horizons, 10 ms to 1 h
part.window_s6010 to 3,600
part.top_n103 to 100

Requesting a profile your account does not own returns 403 with code plan_required.