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.
| Column | Type | Description |
|---|---|---|
| common columns | venue, symbol, venue_symbol, ts_exchange, ts_capture, seq, source, region, as on Schema. source is always fdc_derived. | |
field | enum | Field key. |
variant | string | Output of the field, listed per field below. |
value | float64 | The 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_grid | bool | true for rows on the 100 ms grid, false for event rows. |
emit | enum | grid or event. The same information as on_grid, as a string for readable filters. |
ts_available | int64 | When 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_id | string | On 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 boundaryTand built only from messages withts_capture < T. They can be joined as-of onts_capturewith 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 onts_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.
| Variant | Wide column | Unit | Emit |
|---|---|---|---|
l1 | ofi_ml_l1 | base asset | grid |
l5 | ofi_ml_l5 | base asset | grid |
l10 | ofi_ml_l10 | base asset | grid |
norm | ofi_ml_norm | unitless | grid |
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.
The bucket size is fixed at 00:00 UTC for the whole day, from the 30 complete days before it.
| Variant | Wide column | Unit | Emit |
|---|---|---|---|
value | vpin | 0 to 1 | event on each closed bucket; last value carried on the grid |
cdf | vpin_cdf | 0 to 1 | same |
bucket_size | vpin_bucket_size | base asset | same |
bucket_seq | vpin_bucket_seq | buckets closed since 00:00 UTC | same |
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.
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.
| Variant | Wide column | Unit | Emit |
|---|---|---|---|
bid_size, ask_size | queue_bid_size, queue_ask_size | base asset | grid, plus event when the touch price changes |
bid_count, ask_count | queue_bid_count, queue_ask_count | orders | same |
age_p50_ms | queue_age_p50_ms | ms | same |
life_p50_ms | queue_life_p50_ms | ms | grid |
fill_1s_bid, fill_1s_ask | queue_fill_1s_bid, queue_fill_1s_ask | probability | grid |
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.
| Variant | Wide column | Unit | Emit |
|---|---|---|---|
l1 | cxl_fill_l1 | ratio of sizes | grid |
l5 | cxl_fill_l5 | ratio of sizes | grid |
cxl_count_10s | cxl_count_10s | cancel events | grid |
fill_count_10s | fill_count_10s | fill events | grid |
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.
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.
| Variant | Wide column | Unit | Emit |
|---|---|---|---|
refill_ms | resil_refill_ms | ms | event per sweep |
refill_1s | resil_refill_1s | ratio | event per sweep |
sweep_size | resil_sweep_size | base asset | event per sweep |
med_5m | resil_med_5m | ms | grid |
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.
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.
| Variant | Wide column | Unit | Emit |
|---|---|---|---|
bps_per_musd | lambda_bps_per_musd | bps per USD 1M signed notional | re-estimated every 1 s, carried on the grid |
r2 | lambda_r2 | 0 to 1 | same |
n | lambda_n | buckets | same |
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.
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.
| Variant | Wide column | Unit | Emit |
|---|---|---|---|
buy_100ms, sell_100ms | markout_buy_100ms, markout_sell_100ms | bps | grid, plus per-trade event |
buy_1s, sell_1s | markout_buy_1s, markout_sell_1s | bps | same |
buy_5s, sell_5s | markout_buy_5s, markout_sell_5s | bps | same |
buy_30s, sell_30s | markout_buy_30s, markout_sell_30s | bps | same |
buy_60s, sell_60s | markout_buy_60s, markout_sell_60s | bps | same |
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.
| Variant | Wide column | Unit | Emit |
|---|---|---|---|
active | part_active | count | grid |
top10_share | part_top10_share | share | grid |
top10_net_flow | part_top10_net_flow | base asset | grid |
maker_hhi | part_maker_hhi | 0 to 1 | grid |
new_share | part_new_share | share | grid |
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_normcurl -G https://api.financedatacloud.com/v1/extended \
-H "Authorization: Bearer $FDC_API_KEY" \
-d venue=hyperliquid -d symbol=BTC-PERP \
-d fields=ofi_ml:l5,vpin,lambda \
-d start=2026-09-15T14:30:00Z -d end=2026-09-15T14:40:00Zaws s3 cp \
s3://fdc-archive/extended/venue=hyperliquid/date=2026-09-15/BTC-PERP.parquet .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.
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.
| Parameter | Default | Allowed range |
|---|---|---|
grid.interval_ms | 100 | 10 to 1,000 |
ofi_ml.levels | 1, 5, 10 | Up to 4 cut-offs, 1 to 20 |
ofi_ml.norm_window_s | 60 | 10 to 900 |
vpin.buckets | 50 | 10 to 500 |
vpin.adv_days | 30 | 5 to 90 |
queue.fill_horizon_ms | 1,000 | 100 to 10,000 |
queue.lookback_s | 3,600 | 60 to 86,400 |
cxl_fill.window_s | 10 | 1 to 3,600 |
resil.levels | 5 | 1 to 20 |
resil.sweep_share | 0.5 | 0.2 to 0.9 |
resil.refill_target | 0.9 | 0.5 to 1.0 |
lambda.bucket_ms | 1,000 | 100 to 60,000 |
lambda.window_buckets | 300 | 60 to 7,200 |
markout.horizons | 100ms, 1s, 5s, 30s, 60s | Up to 8 horizons, 10 ms to 1 h |
part.window_s | 60 | 10 to 3,600 |
part.top_n | 10 | 3 to 100 |
Requesting a profile your account does not own returns 403 with code plan_required.