State management of resting orders
Overview
This example shows how to track orders and update them according to the different MBO actions. Order tracking is simpler to implement than a full limit order book: it keeps track of all resting orders, but not the book levels. The book levels such as the top of book (BBO) can be constructed from a map of resting orders, but it requires inefficiently iterating over all the orders.
This example works for a single instrument. To use it with multiple instruments, you will need to extend it to keep separate books for each instrument.
Order events
| Name | Value | Action |
|---|---|---|
| Add | A | Insert a new order into the book. |
| Modify | M | Change an order's price and/or size. |
| Cancel | C | Fully or partially cancel an order from the book. |
| Clear | R | Remove all resting orders for the instrument. |
| Trade | T | An aggressing order traded. Does not affect the book. |
| Fill | F | A resting order was filled. Does not affect the book. |
| None | N | No action: does not affect the book, but may carry flags or other information. |
Trade and Fill actions do not affect the book because all fills will be accompanied by cancel actions that do update the book.
Handling F_LAST
A single event from a publisher can be normalized into multiple records. Databento sets the flag F_LAST to denote the last record for an instrument in an event. The book state should only be examined after a record with F_LAST set.
Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
from dataclasses import dataclass, field
import databento as db
OrderId = int
UnixTimestamp = int
@dataclass(slots=True)
class Order:
side: str
price: int
size: int
ts_event: UnixTimestamp
@dataclass(slots=True)
class PriceLevel:
price: int | None = None
size: int = 0
count: int = 0
@dataclass(slots=True)
class Book:
orders: dict[OrderId, Order] = field(default_factory=dict)
def bbo(self) -> tuple[PriceLevel, PriceLevel]:
best_ask = PriceLevel()
best_bid = PriceLevel()
for order in self.orders.values():
if order.side == "A":
if best_ask.price is None or best_ask.price > order.price:
best_ask = PriceLevel(
price=order.price,
size=order.size,
count=1,
)
elif best_ask.price == order.price:
best_ask.size += order.size
best_ask.count += 1
elif order.side == "B":
if best_bid.price is None or best_bid.price < order.price:
best_bid = PriceLevel(
price=order.price,
size=order.size,
count=1,
)
elif best_bid.price == order.price:
best_bid.size += order.size
best_bid.count += 1
return best_bid, best_ask
def apply(
self,
ts_event: UnixTimestamp,
action: str,
side: str,
order_id: int,
price: int,
size: int,
flags: db.RecordFlags,
) -> None:
# Trade, Fill, or None: no change
if action in ("T", "F", "N"):
return
# Clear book: remove all resting orders
if action == "R":
self.orders.clear()
# Add: insert a new order
elif action == "A":
# For top-of-book publishers, remove previous order associated with this side
if flags & db.RecordFlags.F_TOB:
self.orders = {i: o for i, o in self.orders.items() if o.side != side}
# UNDEF_PRICE indicates the price level was removed. There's no new level to add
if price == db.UNDEF_PRICE:
return
self.orders[order_id] = Order(side, price, size, ts_event)
# Cancel: partially or fully cancel some size from a resting order
elif action == "C":
existing_order = self.orders[order_id]
assert existing_order.size >= size
existing_order.size -= size
# If the full size is cancelled, remove the order from the book
if existing_order.size == 0:
self.orders.pop(order_id)
# Modify: change the price and/or size of a resting order
elif action == "M":
existing_order = self.orders[order_id]
# The order loses its priority if the price changes or the size increases
if existing_order.price != price or existing_order.size < size:
existing_order.ts_event = ts_event
existing_order.size = size
existing_order.price = price
# First, create a historical client
client = db.Historical("$YOUR_API_KEY")
# Next, we will request MBO data starting from the beginning of pre-market trading hours
data = client.timeseries.get_range(
dataset="XNAS.ITCH",
start="2022-08-26T08:00:00",
end="2022-08-26T14:30:00",
symbols="MSFT",
schema="mbo",
)
# Then, we replay the data, updating the book with each record
book = Book()
book_is_ready = False
for mbo in data:
book.apply(mbo.ts_event, mbo.action, mbo.side, mbo.order_id, mbo.price, mbo.size, mbo.flags)
book_is_ready = mbo.flags & db.RecordFlags.F_LAST
# Finally we inspect the final BBO
if book_is_ready:
best_bid, best_ask = book.bbo()
print(f"Best ask {float(best_ask.price) / db.FIXED_PRICE_SCALE} × {best_ask.size}")
print(f"Best bid {float(best_bid.price) / db.FIXED_PRICE_SCALE} × {best_bid.size}") Result
1 2
Best ask 276.07 × 62 Best bid 276.01 × 19