Process L2 (price-level) depth feeds¶
ob-analytics was built around an L3 per-order stream — every event is a
created / changed / deleted action keyed by an order id, and the
reconstruction stages (order classification, aggressiveness, queue position)
all depend on that identity. Many venues — Binance, Kalshi, Polymarket, and
most CCXT sources — publish only L2 aggregated data: price-level
[price, quantity] snapshots and diffs with no order IDs.
What this feed shows
| Property | Value |
|---|---|
| Level | L2 |
| Depth shown | what the file holds |
| Update form | as recorded |
| What can be missed | as recorded |
| After a lost message | — |
| Sequence | if present |
| Clocks | as recorded |
| Crossing | price levels |
| Taker side | the side column, or Lee–Ready |
| Trade tape gaps | as recorded |
| Price grid | you supply it |
| What the book means | as recorded |
| Access | — |
Use it for any price-level data you already have. Don't use it for anything about single orders: an L2 file has none. What each feed shows explains each property and compares every source.
This guide covers the L2 depth-native ingestion path: those feeds produce valid depth / spread / trade analytics without faking per-order state. It is the counterpart to the L3 Bitstamp / LOBSTER paths.
L2 vs L3, in one line¶
A Source declares its
level — a Level:
Level |
Feed | What each row is | Per-order stages |
|---|---|---|---|
L3 |
Market-by-order (Bitstamp, LOBSTER, Databento) | one primitive per resting order | run |
L2 |
Market-by-price (Binance, Kalshi, Polymarket, CCXT) | one price level's aggregate size | skipped |
On the L2 path the loader yields the depth frame directly — a price-level
feed is a depth stream, so there is nothing to reconstruct — and the pipeline
skips the per-order stages (set_order_types, order_aggressiveness, queue
reconstruction) because there is no order identity to key on.
The L2 CSV schema¶
One row per price-level update, each carrying the level's new absolute
resting size (0 removes the level) — not a signed delta. A book snapshot is
simply the opening block of rows.
timestamp,side,price,volume
1700000000000,bid,99.0,5.0 # ─┐ opening snapshot
1700000000000,bid,98.0,8.0 # │
1700000000000,ask,101.0,4.0 # │
1700000000000,ask,102.0,7.0 # ─┘
1700000005000,ask,101.0,2.0 # level 101 now holds 2 (was 4)
1700000015000,ask,101.0,0.0 # level 101 removed
| column | meaning |
|---|---|
timestamp |
receive time — integer epoch (config.timestamp_unit) or any string pandas can parse |
side |
bid / ask (buy / sell and b / a are also accepted) |
price |
the price level (divided by config.price_divisor, then stored as a whole number of tick_size ticks; a price between two ticks raises ConfigError instead of being rounded) |
volume |
the level's new absolute resting size (0 = removed) |
Column names are flexible: side / direction and volume / size /
amount / quantity are all accepted.
Point the pipeline at the file (or a directory containing depth.csv); a
companion trades.csv next to it is picked up automatically.
from ob_analytics import Pipeline
result = Pipeline.from_source("depth_csv").run("my_l2_run/")
result.level # Level.L2
result.depth # the price-level book (timestamp, price, volume, direction)
result.depth_summary # best bid/ask + BPS-bin depth over time
result.events # empty — the per-order stages did not run
What you get — and what's skipped¶
The depth analytics run exactly as they do for L3, because
DepthMetricsEngine already consumes an absolute price-level book:
from ob_analytics.depth import get_spread
get_spread(result.depth_summary) # best bid/ask through time
The per-order stages are skipped with a clear reason — result.events is
an empty (but schema-valid) frame, and result.level is Level.L2 records
why. Anything that needs order identity — order-type classification, order
aggressiveness, order lifecycles, queue position — has nothing to work with on
an aggregated feed and is not attempted.
Trade signs¶
L3 crypto ships the taker side for free; price-level feeds usually don't. When
trades.csv has no side column, the pipeline classifies the aggressor with
Lee–Ready against the reconstructed touch: the last quote strictly before
each trade, falling back to the tick rule at the mid — see
trade signs. A native side column is honored as-is.
Because there are no order IDs, the maker / taker (and event-id) columns are
<NA> — that is expected for L2, not a data-quality failure.
Visualizing an L2 result¶
The gallery adapts to the resolution: an L2 result renders the depth/trades faces (the depth heatmap, price view, depth percentiles, trade tape, trade size) and skips every per-order face rather than erroring.
from ob_analytics.visualization import available_concepts
result.plot("depth_heatmap") # the hero L2 face
sorted(available_concepts(result)) # only the L2-supported concepts
Checking data quality¶
ob-analytics audit <source> --source depth_csv (and
data_quality_summary) work on an L2 result: the crossed-book % is read from
the price-level book, and the per-order metrics report zero. Every L2 source
declares FeedType.PRICE_LEVELS, so a crossed book fails the run: the venue's
own book does not cross.
Try it on the synthetic fixture¶
The bundled toy_l2_depth() / toy_l2_trades() are a tiny, hand-verifiable
snapshot + delta stream — the L2 counterpart to toy_events / toy_trades:
from ob_analytics.datasets import toy_l2_depth, toy_l2_trades
from ob_analytics.depth import depth_metrics, get_spread
summary = depth_metrics(toy_l2_depth())
get_spread(summary)
Related¶
- Trade signs — Lee–Ready / tick / BVC for unlabelled feeds
- Custom components — write a loader for any other format
- What each feed shows — depth, gaps, clocks and trade sides, source by source
- Data quality — matched book, diff feed and price levels
- L2 API reference —
L2DepthLoader,DepthCsvSource, …
Live L2 capture
A live L2 capture writes this schema directly: the ccxt source
records depth.csv and trades.csv for any CCXT venue, including
Kalshi. It also records the market's tick size and size
step in meta.json, and the pipeline uses them, in Python and in
ob-analytics process.