Capture native per-order (L3) crypto data (cryptofeed)¶
cryptofeed streams normalised market data over websockets and maintains the order book for you, applying each venue's snapshot and deltas. It is the per-order complement to the CCXT source: where CCXT covers the widest venue list at price-level detail, cryptofeed can also deliver market-by-order (L3) on the venues that publish it — the feed the reconstruction engine was built for.
What this feed shows
| Property | Bitstamp L3 | Bitfinex L3 | Blockchain.com L3 | Independent Reserve L3 | L2 (Bitstamp, Kraken) |
|---|---|---|---|---|---|
| Level | L3 | L3 | L3 | L3 | L2 |
| Depth shown | top 100 orders a side | top 100 orders a side | not checked (the book held 1 to 34 orders) | whole book | Bitstamp: whole book. Kraken: top 1,000 levels a side, and levels that leave it are kept |
| Orders shown | resting only | resting only | resting only | resting only | — |
| Update form | snapshots, about 10 a second | opening book, then every change | opening book, then every change | REST book newer than the stream, then every change | Bitstamp: REST book after 5 s, then changes. Kraken: changes |
| What can be missed | orders that come and go between snapshots | nothing | nothing | nothing found in testing | a lost message |
| After a lost message | the next snapshot corrects it | reconnects; new opening book | reconnects; new opening book | reconnects; new opening book | drifts until the level changes again |
| Sequence | none | counts every message on the connection, so book rows skip numbers; only rises | counts every message on the connection, so book rows skip numbers; only rises | skips messages for orders cryptofeed does not hold; only rises | none |
| Clocks | venue + receive | receive only | receive only | venue + receive; the opening book receive only | Bitstamp: venue + receive. Kraken: receive only |
| Crossing | matched book; failed the check in testing (0.125% crossed): Bitstamp's own snapshots can briefly show a bid at a resting ask's price | matched book; failed the check in testing (58% crossed; cause not found) | matched book | matched book; failed the check in testing (58% crossed): orders that stayed in the book after they had gone | price levels |
| Trade sides named | maker only, from the tape | neither | neither | maker only, from the tape | — |
| Taker side | venue | venue | venue (not checked) | venue | venue |
| Fills | from the tape | not linked | not linked | from the tape | — |
| Trade tape gaps | a few trades from before the first snapshot | starts with the last 30 trades before the capture | not checked | none found | Bitstamp: a few trades from before the REST book |
| Price grid | fixed (0.01 on BTC/USD) | five significant figures | fixed (0.01 on BTC/USD) | fixed (0.01 on BTC/AUD); some trade prices are off it | fixed (Bitstamp 0.01, Kraken 0.1 on BTC/USD) |
| What the book means | normal | normal | normal | normal | normal |
| Access | public | public | public | public | public; Coinbase needs an API key in cryptofeed 2.4.1; other venues not checked |
Use it for an independent check of the top of the Bitstamp book, and for the
per-order book on Independent Reserve
(see below).
Don't use it for Bitstamp order analysis (use the native
bitstamp source) or for Bitfinex order lifetimes. On
Bitfinex, Blockchain.com and Independent Reserve the book rows skip sequence
numbers without losing any, so audit checks only that the numbers never
go back; a lost message shows as a book_resyncs warning instead (see
Dropped messages and reconnects).
What each feed shows explains each property and compares
every source.
Install the optional [cryptofeed] extra and use the capture verb:
The level comes from the venue, not from a list¶
A cryptofeed exchange declares the channels it supports. The source reads that declaration: a venue offering a per-order book is captured at L3, and every other venue at L2. Nothing is hardcoded, so the choice tracks cryptofeed's own coverage as it changes.
In cryptofeed 2.4 and 2.5 the venues publishing a per-order book are bitstamp, bitfinex, blockchain, and independent_reserve. Coinbase and Bitso publish price-level data only.
| Level | Output | Replays through |
|---|---|---|
| L3 | orders.csv (real venue order IDs) |
the full reconstruction pipeline |
| L2 | depth.csv (absolute size per level) |
the L2 path |
--level forces the choice. Forcing L2 on an L3-capable venue is allowed —
it is a coarser view of the same book. Forcing L3 on a venue that does not
publish one fails, because the only way to satisfy it would be to invent order
IDs that the feed never had.
# A per-order venue, captured at price level instead:
ob-analytics capture cryptofeed --exchange bitstamp --pair BTC-USD --level L2 --out /tmp/cap
# An L2-only venue:
ob-analytics capture cryptofeed --exchange binance --pair BTC-USDT --out /tmp/cap
A capture is a directory of segments with a manifest.json (see
Running for days). Each segment holds:
| File | Contents |
|---|---|
orders.csv |
L3 only: per-order created / changed / deleted events |
depth.csv |
L2 only: price-level updates (volume = new absolute size, 0 removes the level) |
trades.csv |
The trade tape (taker side; feeds trade-sign) |
raw.jsonl |
Raw frames as cryptofeed passed them on (omit with --no-raw). cryptofeed reads prices and sizes as Decimal, and these are written as strings so no digits are lost: a venue's 0.011 is stored as "0.011". For some venues (independent_reserve, blockchain) cryptofeed also reads date and time strings as dates and times; these are written back as ISO 8601 strings, such as "2026-09-28T08:30:15.123456+00:00". These strings come from cryptofeed's parsed value, not the venue's text: Z becomes +00:00, and digits past the microsecond are lost. Any other value or dict key that JSON cannot hold is written as text, and a frame that JSON cannot hold at all is skipped; meta.json records both (see Capture live order-book data) |
meta.json |
Counts + per-run diagnostics (venue, level, book updates, sequence gaps, fills from the trade tape, errors), what the source declares about its feed (source, feed_type, trade_attribution), and what raw.jsonl wrote as text or skipped (raw_text_types, n_raw_frames_skipped) |
How the per-order events are derived¶
cryptofeed's L3 venues do not agree on what a book callback carries, so the source handles both shapes:
- A populated delta —
(order_id, price, quantity)triples, with a quantity of0meaning the order is gone — is mapped entry by entry. This is the venue's own account of what changed, so a venue that models a price move as a removal followed by an add (bitfinex) keeps that meaning: a move loses queue position, and recording it as one order quietly changing price would misstate the queue. - No delta, or an empty one — bitstamp resends the whole book on every
message, and bitfinex, blockchain and independent_reserve all open that way.
The maintained book is diffed against the tracked orders to recover the same
created/changed/deletedvocabulary.
Bitstamp: a top-100 window, not every order¶
cryptofeed does not read Bitstamp's order channel (live_orders). Its Bitstamp
L3 book is the detail_order_book channel: about 10 times a second, a snapshot
of the top 100 bids and the top 100 asks. Three things follow.
- An order can leave the snapshot without leaving the book. When the book
shows 100 orders on a side, an order missing past the last price shown, or at
that price, may just have dropped below the 100th place. The source keeps it
at its last known size and records nothing. When it comes back it is the same
order. It is recorded
deletedonly once a snapshot shows its price again and it is not there, or at the end of the capture. So an order cancelled while out of view stays in the rebuilt book, below the 100th order, until then. - Takers never appear. A taker trades the moment it arrives, so it is never
resting when a snapshot is taken.
trades.csvnames both orders of every trade (Bitstamp sends them), but only the maker can be found inorders.csv. The source declares this astrade_attribution = maker_only, andauditcounts only makers (see Check data quality). No order can be labelled a market order. - Fills come from the trade tape. Between two snapshots, a fill and a cancel
look the same: the order's size drops, or the order is gone. So when a trade
names an order the source is tracking, the source records the fill straight
away as a
changedevent, and an order filled completely is later recordeddeletedat size 0, as the native Bitstamp feed reports a fill. The snapshots and the trades come on separate channels, in either order: - A trade that a snapshot already shows (the snapshot is as late as the trade,
on the venue's clock) is not applied twice;
meta.jsoncounts these astape_fills_already_shown. - A snapshot that shows an order gone before its trade arrives holds the
deletedfor up to 2 seconds of venue time, so the late trade can still report its fill.
An order first seen after it was partly filled cannot be linked to that trade. On a 90-second capture, 40 of 42 trades linked to their maker.
For analysis of Bitstamp orders, use the native bitstamp
source: it reports every order, takers included, and every fill. The cryptofeed
capture is still useful as a check on the top of the native book. Each snapshot
states the top 100 in full, so an error there is corrected within a tenth of a
second, where a stream of changes keeps a lost message until the capture ends.
Independent Reserve: both orders of a trade¶
Independent Reserve's trade messages name the bid and the offer of each trade
(BidGuid and OfferGuid), and the source writes them to trades.csv as
buy_order_id and sell_order_id. Its book reports every change, a fill
included, so a fill reaches the capture twice: from the trade and from the
book. The source records each fill once. The one exception is a full fill that
the book reports before its trade: it reads as a cancel (see below). The two
messages are compared on the time Independent Reserve sent them, which is the
same for a trade and the book change it causes.
- The maker is always named. It was resting, so the book shows it.
- A full fill that the book reports before its trade reads as a cancel. The book's removal is written at once, at the order's last size. Unlike on Bitstamp, it is not held for the trade, because that would write it after rows from later messages. In every capture so far, each trade arrived before the book change it caused.
- The taker is named only when the book shows it. Some takers appear in
the book as new limit orders, and most never do: in 40 minutes, 6 of 26
takers appeared. So the source declares
trade_attribution = maker_only. In a 20-minute capture, all 13 trades were linked to their maker. - Later changes and cancels are not lost. Before 3.0, cryptofeed forgets an order after its first change, so it ignores the order's later fills and its cancel (see What each feed shows). The source uses a subclass of cryptofeed's feed that keeps the order while it rests, so these reach the capture. The trade also reports each fill, and the source records it once.
- The opening book is no older than the stream. The venue serves its REST book from a cache, so the source fetches it again until it is newer than the first stream message (see Dropped messages and reconnects). Otherwise an order cancelled just before the stream started would stay in the book.
- Trades from the venue's other markets are left out of
trades.csv. A BTC-AUD capture also gets BTC-NZD and BTC-SGD trades, at prices in those currencies: 3 of 26 trades in 40 minutes. The source writes a trade totrades.csvonly when it is for the capture's pair, andmeta.jsoncounts the others asother_market_trades. Their raw frames are still inraw.jsonl. The venue keeps one book for all currencies, so these trades name orders in the capture, and the source still records their fills, at the order's price in the capture's currency. See What each feed shows.
Order IDs are the venue's own throughout, exactly as published — integers on bitstamp, bitfinex and blockchain, UUID strings on independent_reserve. The shared schema keys orders by identity rather than by integer, so nothing is re-labelled on the way through.
At shutdown every order still resting is closed out with a synthetic
deleted, so each ID in orders.csv has a complete lifecycle.
Dropped messages and reconnects¶
cryptofeed owns reconnection. It checks the venue's sequence numbers on every
message, and on a lost one it reconnects and takes a new opening book. The
capture cannot see the reconnect, but it can see the new opening book after
changes to the old one. It brings its book into line with the new one and
counts it as book_resyncs in meta.json. The changes between the two
connections were missed. A capture of several segments adds the counts up in
manifest.json, and audit warns about them in the book_resyncs check.
Changes that come before the first opening book, such as on Bitstamp L2, are
not a resync. Bitstamp's L3 channel sends the whole book every time, so it
never counts one.
On Independent Reserve, cryptofeed takes the opening book from the venue's REST interface when the first stream message arrives. The venue serves that book from a cache, so it can be a few seconds older than the stream, and an order cancelled in between would stay in the book until the capture ended. The source fetches the book again, once a second, until it was created at least 3 s after the first message, at the start and after each reconnect. The stream waits meanwhile and loses nothing, so the first rows arrive 3 to 6 s later than on other venues. If no new enough book comes in 15 tries, the source uses the last one and logs a warning.
The sequence numbers are recorded per row in both orders.csv and
depth.csv, but they skip although no message was lost: on Bitfinex and
Blockchain.com they count every message on the connection, trades and
heartbeats too, and on Independent Reserve cryptofeed passes on no message about
an order it does not hold. So the source declares them as only rising
(sequence_kind is monotonic in meta.json), and audit checks only that
they never go back. Each run also reports sequence_out_of_order, how many
times the number went back, in meta.json. On Bitfinex and Blockchain.com the
count starts again on a new connection. The capture counts that step back as a
sequence_restarts, not as out of order, and audit leaves it out of
sequence_out_of_order: the book_resyncs warning already reports the
reconnect.
For the authoritative check, replay the capture and score it:
from ob_analytics import SequenceKind
from ob_analytics.analytics import detect_sequence_gaps
from ob_analytics.bitstamp import BitstampLoader
from ob_analytics.config import PipelineConfig
events = BitstampLoader(config=PipelineConfig(track_sequence=True)).load("orders.csv")
print(detect_sequence_gaps(events, kind=SequenceKind.MONOTONIC))
A venue that publishes no sequence number is never scored, and reports zero.
Flags¶
| Flag | Meaning |
|---|---|
--exchange |
cryptofeed venue id (bitstamp, binance, coinbase, …) |
--pair |
Symbol in cryptofeed notation (e.g. BTC-USD) |
--level |
Force L2 or L3; omit to discover it from the venue |
Which source for which venue¶
- Bitstamp orders: use the native
bitstampsource for analysis. It shows every order and every fill, where cryptofeed's Bitstamp book shows the top 100 orders a side and no takers (see above). A cryptofeed capture of the same period is still worth making as an independent check of the top of the native book: each snapshot states the top 100 in full, so it does not keep an order the native stream lost. - bitfinex, blockchain or independent_reserve orders: use cryptofeed. It is the only per-order source for these venues. Read What each feed shows first: Bitfinex's book is a top-100 window too, and on all three the book rows skip sequence numbers without losing messages.
- Price levels and prediction markets: use CCXT. It covers the most venues, and Kalshi and Polymarket, which cryptofeed does not.
See also¶
- Capture CCXT venues — the price-level source covering the widest venue list
- Capture live data — the capture framework and writing a bespoke venue
- Process L2 (price-level) feeds — what a captured
depth.csvflows through - Check data quality — run
auditon the captured output. A capture records which source made it, soaudit --source bitstampon a cryptofeed L3 capture checks it against what cryptofeed can show