PF
(See BTP Body Encoding).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.
Byte Offset | Byte Length | Type | Name | Description |
---|---|---|---|---|
0 | 1 | char | messageType | Always T |
1 | 8 | uint64 | ackId | Matching engine acked sequence |
9 | 8 | uint64 | productId | Product ID |
17 | 1 | char | takerSide | Taker side: B = bid, A = ask |
18 | 8 | int64 | price | Price in ticks |
26 | 4 | uint32 | quantity | Quantity |
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.
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 Offset | Byte Length | Type | Name | Description |
---|---|---|---|---|
0 | 1 | char | messageType | Always L |
1 | 8 | uint64 | ackId | Latest matching engine acked sequence number |
9 | 8 | uint64 | productId | Product ID |
17 | 1 | char | side | Side: B = bid, A = ask |
18 | 8 | int64 | price | Price in ticks |
26 | 4 | uint32 | quantity | Quantity |
30 |
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 Offset | Byte Length | Type | Name | Description |
---|---|---|---|---|
0 | 1 | char | messageType | Always B |
1 | 8 | uint64 | lastAckId | Last ackId |
9 | 8 | uint64 | productId | Product ID |
17 | 4 | uint32 | bidsLength | Number of bytes for bid levels |
21 | bidsLength | Levels | bidLevels | See Book Level |
21 + bidsLength | 4 | uint32 | asksLength | Number of bytes for ask levels |
25 + bidsLength | asksLength | Levels | askLevels | See Book Level |
25 + bidsLength + asksLength |
Byte Offset | Byte Length | Type | Name | Description |
---|---|---|---|---|
0 | 8 | int64 | price | Price in ticks |
8 | 4 | uint32 | quantity | Quantity |
12 |
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 Offset | Byte Length | Type | Name | Description |
---|---|---|---|---|
0 | 1 | char | messageType | Always X |
1 | 8 | uint64 | ackId | Matching engine acked sequence |
9 | 8 | uint64 | productId | Product ID |
17 | 8 | int64 | price | Price in ticks |
25 | 4 | uint32 | quantity | Quantity |
29 |
Assuming there are 10 contiguous levels already on the book, you would see a book message similar to the following.
{
"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, you’ll see a book message like this.
{
"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, you would not receive a Level
update.
{
"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.
{
"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.