Pricefeed

The pricefeed protocol sends incremental level and trade updates as needed and book snapshots every 10 seconds. When connecting to the pricefeed intra-day, the customer must wait for a book snapshot to apply incremental updates. In other words, after connecting to the pricefeed, the customer should ignore level updates until they receive a book snapshot. Once that happens they can correctly apply level updates. This process is independent of any market state update as the market state message is not sufficient to construct the book. For example if the customer receives a market open message, that could be the market opening from a halted state which means there is existing book state and the book is NOT clear.

Ack IDs are monotonically incremented at the engine but may appear out of sequence, repeated, and gapped in the pricefeed due in part to inside-out sequencing. Use sequence IDs to determine dropped or duplicated messages.

See Examples for some practical examples.

Subscriptions

From BTP version 4, pricefeed customers can manage a per-session product subscription set with the Subscribe, Unsubscribe, and Subscribe All requests. These requests and List Products are the only application-level messages a pricefeed customer sends. In earlier protocol versions the pricefeed has no customer-to-server application messages; any application message sent on a pre-v4 session fails to parse and the connection is disconnected with reason 0x05 (Failed to parse message).

Subscription Set Semantics

  • Every session starts subscribed to all products, at every protocol version. A version 4 session that never sends a subscription request keeps receiving every product's traffic, exactly like a pre-v4 session.
  • The first Subscribe narrows the session from the all-products default to exactly the requested product set.
  • Subsequent Subscribe requests add (union) products into the session's set. Unsubscribe requests remove products from the set; unsubscribing a product not in the set is a no-op.
  • Subscribe All returns the session to the all-products state. It is a no-op on a session already subscribed to all products, including a fresh session at the login default.
  • An explicit subscription set only ever contains product IDs the customer has named: products that list later are not automatically added. The all-products state does include products that list later.
  • Unsubscribe on the all-products state is a no-op: it does not subtract from "all products" and the session stays subscribed to everything. A customer that wants all-but-a-few products should instead subscribe to the complement set, discoverable with List Products.
  • A Subscribe with a productCount of 0 on the all-products state narrows the session to the empty set, which receives no product traffic until a later Subscribe or Subscribe All; on an explicit set it adds nothing. An Unsubscribe with a productCount of 0 changes nothing. A session also reaches the empty set by unsubscribing every product in its set.
  • Duplicate product IDs within one request count once.
  • Unknown product IDs are accepted and retained, never rejected. An unknown product ID matches no traffic and produces no snapshot unless a product with that ID lists later.

The subscription set filters every product-scoped message: Trade, Level, Book, Block Trade, and Market State messages for unsubscribed products are not sent. Disconnect messages and List Products replies are never filtered by the subscription set.

Filtering happens before sequence IDs are assigned, so a filtered message does not create a sequence ID gap. Sequence IDs number only the messages sent on the connection, and the drop and duplicate detection described above applies unchanged to a narrowed session.

Snapshots on Subscribe

Each product newly added to the subscription set is answered, on the same connection, with the product's current market state followed by a fresh book snapshot. These are the same messages the periodic snapshot broadcast carries, and they give the customer a consistent basis for subsequent incremental updates. A product with no market state yet receives only the book snapshot. The subscription takes effect before the snapshot is sent, so an incremental update for a newly subscribed product may arrive ahead of that product's snapshot: as when connecting intra-day, ignore level updates for a product until its book snapshot arrives. A product that is unsubscribed and later re-subscribed receives a fresh snapshot the same way; incremental continuity is not maintained while a product is unsubscribed.

A product is newly added only if it was not already in the session's subscription set:

  • The first Subscribe on a session still at the all-products default narrows it to the requested set, and every requested product is snapshotted. The session's initial basis for all products comes from the market state replay sent at login, which is not filtered by the subscription set, and the periodic snapshot broadcast, as for any session.
  • A Subscribe for a product already in the set sends no snapshot for that product.
  • Subscribe All on a narrowed session snapshots every known product that was not already in the set. On a session already at the all-products state, including a fresh session at the login default, it sends no snapshots.
  • Unsubscribe never sends a snapshot.
  • Unknown product IDs never produce a snapshot.

Subscription requests are not otherwise acknowledged; the snapshots for newly added products are the only direct response.

Subscribe

  • BTP version 4 onward.
Byte OffsetByte LengthTypeNameDescription
01charmessageTypeAlways S
14uint32productCountNumber of product IDs that follow
58 × productCountuint64[]productIdsProduct IDs to subscribe to
5 + 8 × productCount

The header's bodyLength is a uint16, which caps a single request at 8,191 product IDs ((65535 − 5) / 8, rounded down). Customers with larger sets send several Subscribe requests, which union into the session's set, or send Subscribe All.

productCount must match the number of product IDs in the body. A Subscribe or Unsubscribe whose body length disagrees with its productCount fails to parse, and the connection is disconnected with reason 0x05 (Failed to parse message).

Unsubscribe

  • BTP version 4 onward.
Byte OffsetByte LengthTypeNameDescription
01charmessageTypeAlways U
14uint32productCountNumber of product IDs that follow
58 × productCountuint64[]productIdsProduct IDs to unsubscribe from
5 + 8 × productCount

Subscribe All

  • BTP version 4 onward.
Byte OffsetByte LengthTypeNameDescription
01charmessageTypeAlways A
1

List Products

From BTP version 4, a customer can discover the products currently available for subscription with a List Products request. The request carries a customer-chosen listProductsId, opaque to the server and echoed verbatim on every message of the reply, so a customer with several listings in flight (or one straddling a reconnect) can correlate replies to requests. The server does not check listProductsId for uniqueness, so use a distinct value for each listing in flight. The server replies on the same connection with one Product Listing message per product, terminated by a Product Listing End message carrying the total count, so the customer can detect completion and short reads.

  • The listing is point-in-time: it reflects the products available when the request was handled, and products that list later are not announced with further Product Listing messages. A session that wants every product including future listings should use Subscribe All instead of subscribing to a listed set.
  • The listing is never filtered by the session's subscription set, so a narrowed (even empty) session still receives the full listing. The request does not modify the subscription set.
  • The pricefeed does not throttle or reject List Products requests: each request is answered with the full listing followed by a Product Listing End. If the connection is lost before the End arrives, discard the partial listing and request it again after reconnecting.
  • A Product Listing carries only the productId. Resolve product IDs to symbols and specifications with the Product API.

List Products Request

  • BTP version 4 onward.
Byte OffsetByte LengthTypeNameDescription
01charmessageTypeAlways Q
18uint64listProductsIdListing ID from customer, echoed on every reply message
9

Product Listing

  • BTP version 4 onward.

One Product Listing is sent per available product in reply to a List Products request.

Byte OffsetByte LengthTypeNameDescription
01charmessageTypeAlways P
18uint64listProductsIdListing ID from the request
98uint64productIdProduct ID
17

Product Listing End

  • BTP version 4 onward.

A Product Listing End terminates the reply to a List Products request.

Byte OffsetByte LengthTypeNameDescription
01charmessageTypeAlways E
18uint64listProductsIdListing ID from the request
94uint32productCountNumber of Product Listing messages sent for this listing
13

Trade

Byte OffsetByte LengthTypeNameDescription
01charmessageTypeAlways T
18uint64ackIdMatching engine acked sequence
98uint64productIdProduct ID
171chartakerSideTaker side: B = bid, A = ask
188int64pricePrice in ticks
264uint32quantityQuantity
30

Customers are not expected to mutate the book upon receiving a trade message. Each trade message will be accompanied with a corresponding level update which the customer can use to mutate the book. For example, if a Bid is submitted for 10@9015 and a subsequent Ask is submitted for 20@9015 the customer can see the following messages:

{
  "messageType": "Level",
  "ackId": 1,
  "price": 9015,
  "side": "Bid",
  "productId": 4,
  "quantity": 10
}
{
  "messageType": "LastTrade",
  "ackId": 2,
  "price": 9015,
  "productId": 4,
  "takerSide": "Ask",
  "quantity": 10
}
{
  "messageType": "Level",
  "ackId": 3,
  "price": 9015,
  "side": "Bid",
  "productId": 4,
  "quantity": 0
}
{
  "messageType": "Level",
  "ackId": 4,
  "price": 9015,
  "side": "Ask",
  "productId": 4,
  "quantity": 10
}

Note that the previous example also applies to calendar spreads. More explicitly, there are no extra pricefeed messages for calendar spread events. They all operate on the spread product only.

Level

A level update reflects the current quantity at the respective price level. A cleared level is represented by a zero quantity. Trade updates from a match event are sent before the respective level updates. Ack IDs on level messages represent the latest available ack ID when we calculated the book update.

Byte OffsetByte LengthTypeNameDescription
01charmessageTypeAlways L
18uint64ackIdLatest matching engine acked sequence number
98uint64productIdProduct ID
171charsideSide: B = bid, A = ask
188int64pricePrice in ticks
264uint32quantityQuantity
30

Book

Only the top 10 levels on each side are generated. It is important to note that Level updates are NOT sent when a level goes out of scope (i.e. there are 10 or more "better" orders on that side). For example, if there are 10 contiguous bids on price levels 9000 through 9009 and someone bids at 9010, no message will be sent for level 9000. The next book update will not contain level 9000, but the order still exists! Additionally, if level 9000 is deleted we will not send a level update as it is out of scope in that scenario. If it comes back into scope (re-enters the top 10 bids) then we will send level updates again.

Byte OffsetByte LengthTypeNameDescription
01charmessageTypeAlways B
18uint64lastAckIdLast ackId
98uint64productIdProduct ID
174uint32bidsLengthNumber of bytes for bid levels
21bidsLengthLevelsbidLevelsSee Book Level
21 + bidsLength4uint32asksLengthNumber of bytes for ask levels
25 + bidsLengthasksLengthLevelsaskLevelsSee Book Level
25 + bidsLength + asksLength

Book Level

Byte OffsetByte LengthTypeNameDescription
08int64pricePrice in ticks
84uint32quantityQuantity
12

Block Trade

A block trade is very similar to a Trade message except that it does not have takerSide and these messages do not interact with the book.

Byte OffsetByte LengthTypeNameDescription
01charmessageTypeAlways X
18uint64ackIdMatching engine acked sequence
98uint64productIdProduct ID
178int64pricePrice in ticks
254uint32quantityQuantity
29

Examples

Level Goes Out of Scope

Assuming there are 10 contiguous levels already on the book, a book message similar to the following would be received.

{
  "asks": [],
  "bids": [
    [10000, 10],
    [10001, 10],
    [10002, 10],
    [10003, 10],
    [10004, 10],
    [10005, 10],
    [10006, 10],
    [10007, 10],
    [10008, 10],
    [10009, 10]
  ],
  "lastAckId": 7158621609438216263,
  "productId": 12,
  "messageType": "Book"
}

After another bid is submitted for 10@10010, a book message like this would be received.

{
  "asks": [],
  "bids": [
    [10001, 10],
    [10002, 10],
    [10003, 10],
    [10004, 10],
    [10005, 10],
    [10006, 10],
    [10007, 10],
    [10008, 10],
    [10009, 10],
    [10010, 10]
  ],
  "lastAckId": 7158621609438216264,
  "productId": 12,
  "messageType": "Book"
}

Notice how the 10000 level is not in the book snapshot, but is still on the book! If the original order at the 10000 level were modified, no Level update would be received.

Aggressor Order Full Fill

  • Submit bid for 20@10000
  • Submit ask for 10@10000
{
    "ackId": 7158621609438216346,
    "price": 10000,
    "productId": 12,
    "quantity": 20,
    "side": "Bid",
    "messageType": "Level"
}
{
    "ackId": 7158621609438216348,
    "price": 10000,
    "productId": 12,
    "quantity": 10,
    "takerSide": "Ask",
    "messageType": "LastTrade"
}
{
    "ackId": 7158621609438216348,
    "price": 10000,
    "productId": 12,
    "quantity": 10,
    "side": "Bid",
    "messageType": "Level"
}

There is no Level update for the aggressor order as it is fully filled and never rests on the book.

Aggressor Order Partial Fill

  • Submit bid for 10@15000
  • Submit ask for 20@15000
{
    "ackId": 7158621609438216349,
    "price": 15000,
    "productId": 12,
    "quantity": 10,
    "side": "Bid",
    "messageType": "Level"
}
{
    "ackId": 7158621609438216351,
    "price": 15000,
    "productId": 12,
    "quantity": 10,
    "takerSide": "Ask",
    "messageType": "LastTrade"
}
{
    "ackId": 7158621609438216351,
    "price": 15000,
    "productId": 12,
    "quantity": 0,
    "side": "Bid",
    "messageType": "Level"
}
{
    "ackId": 7158621609438216351,
    "price": 15000,
    "productId": 12,
    "quantity": 10,
    "side": "Ask",
    "messageType": "Level"
}

Since the aggressor order was only partially filled, we get a Level update for both the aggressor and an update which clears out the original passive order since it was fully filled.

Start Trading

Leveraged spot, perpetuals, futures, options, and prediction markets, all on one CFTC-regulated exchange with crypto margin and settlement.