An order book is a list of buy and sell orders for a specific security or financial instrument, organized by price level. It is used by traders to determine the best price to buy or sell a security.
In this post, we will look at the different parts of an order book and how to implement them using Python.
Order Book Components
An order book consists of two main components: the bid side and the ask side.
Bid Side
The bid side of the order book contains all the buy orders for a security, organized by price level. The highest bid price is at the top of the order book, and the lowest bid price is at the bottom.
Ask Side
The ask side of the order book contains all the sell orders for a security, organized by price level. The lowest ask price is at the top of the order book, and the highest ask price is at the bottom.
Implementing an Order Book
In essence, an order book is a list of price levels, where each price level contains a list of orders.
To implement the list of orders at each price level, we can use a linked list data structure. Here is an example implementation of the order and the order list:
class OrderInfo: def __init__(self, id, symbol="", type="", price=0, quantity=0, status="", timestamp="", userid=""): self.id = id self.type = type self.price = price self.quantity = quantity self.status = status self.timestamp = timestamp self.userid = userid self.symbol = symbol class OrderNode: def __init__(self, order_info): self.order_info = order_info self.next = None self.prev = None class OrderList: def __init__(self, symbol, type, price): self.head = None self.tail = None self.size = 0 self.symbol = symbol self.type = type self.price = price def add_order(self, order_info): order_node = OrderNode(order_info) if self.head == None: self.head = order_node self.tail = order_node else: self.tail.next = order_node order_node.prev = self.tail self.tail = order_node self.size += 1 return order_node def remove_order(self, order_node): if order_node.prev: order_node.prev.next = order_node.next else: self.head = order_node.next if order_node.next: order_node.next.prev = order_node.prev else: self.tail = order_node.prev self.size -= 1 def is_empty(self): return self.size == 0
In this implementation, we have three classes: OrderInfo, OrderNode, and OrderList. OrderInfo represents the information of an order, OrderNode represents a node in the linked list, and OrderList represents the list of orders at a specific price level. OrderList has methods to add and remove orders. It manages the linked list internally so we can easily add and remove orders.
So now, to represent the order book, we can create a class that contains two sorted dictionaries, one for the buy orders and one for the sell orders. Each dictionary key is sorted by price, and each price has a list of orders (OrderList).
from sortedcontainers import SortedDict class OrderBook: def __init__(self, symbol): # ticker symbol self.symbol = symbol # sorted by price, each price has a list of orders (OrderList) # key: price, value: OrderList self.buy_orders = SortedDict() self.sell_orders = SortedDict() # key: order_id, value: OrderNode self.all_orders = dict()
To add an order to the order book, we first check if the order is a buy order or a sell order. Then we find the corresponding OrderList for the price level and just simply add the order to the list.
def add_order(self, order_info): if order_info.id in self.all_orders: return None # Grab the OrderList for the price level if order_info.type == "buy": if order_info.price not in self.buy_orders: # create a new OrderList if the price level does not exist order_list = OrderList(self.symbol, order_info.type, order_info.price) self.buy_orders[float(order_info.price)] = order_list else: order_list = self.buy_orders[float(order_info.price)] else: if order_info.price not in self.sell_orders: order_list = OrderList(self.symbol, order_info.type, order_info.price) self.sell_orders[float(order_info.price)] = order_list else: order_list = self.sell_orders[float(order_info.price)] # actually add the new order to the OrderList here new_order = order_list.add_order(order_info) self.all_orders[order_info.id] = new_order
Removing an order is simpler since we already have the OrderNode reference. We just need to remove the order from the OrderList and remove the order from the all_orders dictionary.
def remove_order(self, order): if order.id not in self.all_orders: return None order_node = self.all_orders[order.id] if order.type == "buy": order_list = self.buy_orders[float(order.price)] else: order_list = self.sell_orders[float(order.price)] order_list.remove_order(order_node) del self.all_orders[order.id]
How Trades Happen
When a new order is added to the order book, it may match with existing orders. For example, if a new buy order is added at a price higher than the lowest ask price, a trade will happen.
To implement this, we can add a method to the OrderBook class that checks for matching orders and executes trades.
def match_orders(self): if not self.buy_orders or not self.sell_orders: return None trades = [] while self.buy_orders and self.sell_orders: buy_price = list(self.buy_orders.keys())[-1] # highest buy price sell_price = list(self.sell_orders.keys())[0] # lowest sell price # buy price is higher than sell price, so we have a match if buy_price >= sell_price: buy_order_list = self.buy_orders[buy_price] sell_order_list = self.sell_orders[sell_price] while not buy_order_list.is_empty() and not sell_order_list.is_empty(): buy_order = buy_order_list.head sell_order = sell_order_list.head buy_quantity = buy_order.order_info.quantity sell_quantity = sell_order.order_info.quantity # equal amounts of buy and sell -> both sides filled if buy_quantity == sell_quantity: buy_order.order_info.status = "filled" sell_order.order_info.status = "filled" new_trade = TradeInfo(...) buy_order_list.remove_order(buy_order) sell_order_list.remove_order(sell_order) # buy quantity is greater than sell quantity, sell side filled # buy side partially filled, remove sell order, update buy order elif buy_quantity > sell_quantity: buy_order.order_info.quantity -= sell_quantity sell_order.order_info.status = "filled" new_trade = TradeInfo(...) sell_order_list.remove_order(sell_order) # sell quantity is greater than buy quantity, buy side filled # sell side partially filled, update sell order, remove buy order else: sell_order.order_info.quantity -= buy_quantity buy_order.order_info.status = "filled" new_trade = TradeInfo(...) buy_order_list.remove_order(buy_order) trades.append(new_trade) # if this price level is empty, remove it from the order book if buy_order_list.is_empty(): del self.buy_orders[buy_price] if sell_order_list.is_empty(): del self.sell_orders[sell_price] else: break return trades
In this method , we check if the highest buy price is greater than or equal to the lowest sell price. If there is a match, we iterate through the orders at that price level and execute trades. If the buy quantity is equal to the sell quantity, both orders are filled. If the buy quantity is greater than the sell quantity, the sell order is filled, and the buy order is partially filled. Vice versa for the sell quantity. We then remove the filled orders from the order book and create a new trade object that represents the executed trades.
Testing
import random from orderbook import OrderBook from order import OrderInfo from datetime import datetime, timedelta def test_orderbook_random(order_count=100000): test_ticker_symbol = 'BTCUSD' test_user_id = "user1-test" order_book = OrderBook(test_ticker_symbol) current_time = datetime.now() sell_price_median = 110.0 buy_price_median = 80.0 trades = [] for i in range(order_count): new_sell_price = random.normalvariate(mu=sell_price_median, sigma=5.0) new_buy_price = random.normalvariate(mu=buy_price_median, sigma=5.0) timestamp = current_time + timedelta(seconds=random.randint(0, 3600)) current_time = timestamp quantity = random.randint(1, 100) order_type = 'sell' if random.random() > 0.5 else 'buy' price = int(new_sell_price if order_type == 'sell' else new_buy_price) order_id = f"{order_type}-{price}-{quantity}-{timestamp}-{test_user_id}" order_info = OrderInfo(...) trade_results = order_book.add_order(order_info) for trade_result in trade_results: trades.append(trade_result) for trade in trades: print('sold', trade.quantity, trade.price) order_book.print()
In this test, we generate random buy and sell orders with random prices and quantities. We then add these orders to the order book and execute trades. The trades are printed at the end.
The orderbook will look something like this at the end of the test:
Conclusion
There are many more details and optimizations that can be made to improve the performance and efficiency of an order book implementation (like not using Python). There are also many other features that can be added to an order book, such as order types, and other order matching algorithms. But I hope this post gives you a good starting point to understand the basics of an order book and how the components work together.
I hope you found this post helpful. Thank you for reading!