Watch orderbook
Depending on your trading strategy, keeping track of the current orderbook can be essential. The orderbook is a list of all the unmatched orders, divided into the bids (buy orders) and the asks (sell orders).
We'll use the Indexer WebSockets data streams for this.
Subscribe to the Orders channel
Lets take as reference the previous section. Subscribe to the Orders channel.
def handler(ws: IndexerSocket, message: dict):
if message["type"] == "connected":
# Subscribe.
ws.orders.subscribe(ETH_USD)
print(message)
Parse the update messages
Grab the bids and asks lists from the incoming messages. Each incoming bid and ask entry is the updated level in the orderbook. Each level is associated with a certain price and a total size. The total size is the current aggregated orders size for that price.
def handler(ws: IndexerSocket, message: dict):
if message["type"] == "connected":
ws.order_book.subscribe(ETH_USD, False)
elif message["channel"] == "v4_orderbook" and "contents" in message:
# Bids levels.
if "bids" in contents:
for bid in contents["bids"]:
price = bid["price"]
size = bid["size"]
# Asks levels.
if "asks" in contents:
for ask in contents["asks"]:
price = ask["price"]
size = ask["size"]
Keeping track
On a continuous loop, keep recording all the incoming bids and asks and update your local orderbook.
def handler(ws: IndexerSocket, message: dict):
if message["type"] == "connected":
ws.order_book.subscribe(ETH_USD, False)
elif message["channel"] == "v4_orderbook" and "contents" in message:
# Modify the above snippet.
# For full snapshot (initial subscribed message), reset the orderbook.
if message["type"] == "subscribed":
orderbook["bids"] = {}
orderbook["asks"] = {}
# Process bids levels.
if "bids" in contents:
for bid in contents["bids"]:
process_price_level(bid, "bids")
# Process asks levels.
if "asks" in contents:
for ask in contents["asks"]:
process_price_level(ask, "asks")
# Orderbook state. Levels are stored as [price, size, offset].
orderbook = {
"bids": {},
"asks": {}
}
def process_price_level(level, side):
"""Process a single price level (bid or ask)"""
if isinstance(level, dict):
# Full snapshot format
price = level["price"]
size = level["size"]
offset = level.get("offset", "0")
else:
# Incremental update format
price = level[0]
size = level[1]
offset = level[2] if len(level) > 2 else "0"
# Update local orderbook.
if float(size) > 0:
orderbook[side][price] = [price, size, offset]
elif price in orderbook[side]:
del orderbook[side][price]
Uncrossing the orderbook
Given the decentralized nature of dYdX, sometimes, some of the bids will be higher than some of the asks.
If trader needs the orderbook uncrossed, then one way is to use the order of messages as a logical timestamp. That is, when a message is received, update a global locally-held offset. Each WebSockets update has a message-id
which is a logical offset to use. Using a timestamp is also an option.
# In the handler() function
# ...
# Process asks levels.
if "asks" in contents:
for ask in contents["asks"]:
process_price_level(ask, "asks")
# Uncross the orderbook.
uncross_orderbook()
def get_sorted_book():
"""Get sorted lists of bids and asks"""
bids_list = list(orderbook["bids"].values())
asks_list = list(orderbook["asks"].values())
bids_list.sort(key=lambda x: float(x[0]), reverse=True)
asks_list.sort(key=lambda x: float(x[0]))
return bids_list, asks_list
def uncross_orderbook():
"""Remove crossed orders from the orderbook"""
bids_list, asks_list = get_sorted_book()
if not bids_list or not asks_list:
return
top_bid = float(bids_list[0][0])
top_ask = float(asks_list[0][0])
while bids_list and asks_list and top_bid >= top_ask:
bid = bids_list[0]
ask = asks_list[0]
bid_price = float(bid[0])
ask_price = float(ask[0])
bid_size = float(bid[1])
ask_size = float(ask[1])
bid_offset = int(bid[2]) if len(bid) > 2 else 0
ask_offset = int(ask[2]) if len(ask) > 2 else 0
if bid_price >= ask_price:
# Remove older entry.
if bid_offset < ask_offset:
bids_list.pop(0)
elif bid_offset > ask_offset:
asks_list.pop(0)
else:
# Same offset, handle based on size.
if bid_size > ask_size:
asks_list.pop(0)
bid[1] = str(bid_size - ask_size)
elif bid_size < ask_size:
ask[1] = str(ask_size - bid_size)
bids_list.pop(0)
else:
# Both filled exactly.
asks_list.pop(0)
bids_list.pop(0)
else:
# No crossing.
break
if bids_list and asks_list:
top_bid = float(bids_list[0][0])
top_ask = float(asks_list[0][0])
# Update the orderbook with uncrossed data.
orderbook["bids"] = {bid[0]: bid for bid in bids_list}
orderbook["asks"] = {ask[0]: ask for ask in asks_list}
Additional logic
Now with an always up-to-date orderbook, implement your trading strategy based on this data. For simplicity here, we'll just print the current state.
def print_orderbook():
"""Print n levels"""
bids_list, asks_list = get_sorted_book()
print(f"\n--- Orderbook for {ETH_USD} ---")
# Print asks.
for price, size in reversed(asks_list):
print(f"ASK: {price:<12} | {size:<16}")
print("----------------")
# Print bids.
for price, size in bids_list:
print(f"BID: {price:<12} | {size:<16}")
print("")
# ...
print_orderbook()