Simple Order Book In Python

July 11, 2024

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.

Orderbook Example

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.

Price Level List

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:

Price Level List

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!