Found while running OrderBook through an open-source matching-engine benchmark, the Matching Engine Performance Challenge — it cross-checks engines against the byte-identical consensus of other open source engines. The matching itself is correct, but auditing the book through the benchmark needs a way to read resting depth, and the engine's public depth accessor crashes the moment it's pointed at a real level — querying it took this one-line fix.
OrderBook.get_volume_at_price(side, price) raises AttributeError for any price that actually has volume. After the price_exists guard passes, both branches call get_price on the side's OrderTree (orderbook/orderbook.py:184 for bids, :189 for asks):
if side == 'bid':
volume = 0
if self.bids.price_exists(price):
volume = self.bids.get_price(price).volume # <-- no such method
return volume
Cause. OrderTree (orderbook/ordertree.py) has no get_price method. The accessor for a price level is get_price_list (ordertree.py:23), which returns the OrderList at that price; get_price simply doesn't exist on the class. So self.bids.get_price(price) blows up with AttributeError: 'OrderTree' object has no attribute 'get_price' before .volume is ever read.
This only fires on a populated level. get_volume_at_price reaches the bad call solely through the price_exists(price) guard — when no order rests at that price the guard is false, the function returns the pre-set volume = 0, and the typo is never exercised. That's why the function looks fine until you query a price that has resting size, which is exactly what a depth read does.
Repro. Rest a single bid, then ask for its volume:
from orderbook import OrderBook
ob = OrderBook()
ob.process_order({'type':'limit','side':'bid','quantity':5,'price':100,'trade_id':1}, False, False)
ob.get_volume_at_price('bid', 100)
# AttributeError: 'OrderTree' object has no attribute 'get_price'
(get_volume_at_price('bid', 100) against an empty book returns 0 and never crashes, which is what hides the bug.)
Fix. Call the method that exists at both sites (orderbook.py:184 and :189):
volume = self.bids.get_price_list(price).volume # was get_price(...)
get_price_list(price) returns the OrderList resting at that price (ordertree.py:23-24), and OrderList.volume (orderbook/orderlist.py:15) is the per-level share volume kept in sync on every append/remove — i.e. exactly the number get_volume_at_price is meant to return. With this change the call returns the resting depth (e.g. 8 after two bids of 5 and 3 at the same price) instead of raising; it only fixes which method name is invoked, not what the book holds.
Happy to share the failing workload.
Found while running OrderBook through an open-source matching-engine benchmark, the Matching Engine Performance Challenge — it cross-checks engines against the byte-identical consensus of other open source engines. The matching itself is correct, but auditing the book through the benchmark needs a way to read resting depth, and the engine's public depth accessor crashes the moment it's pointed at a real level — querying it took this one-line fix.
OrderBook.get_volume_at_price(side, price)raisesAttributeErrorfor any price that actually has volume. After theprice_existsguard passes, both branches callget_priceon the side'sOrderTree(orderbook/orderbook.py:184for bids,:189for asks):Cause.
OrderTree(orderbook/ordertree.py) has noget_pricemethod. The accessor for a price level isget_price_list(ordertree.py:23), which returns theOrderListat that price;get_pricesimply doesn't exist on the class. Soself.bids.get_price(price)blows up withAttributeError: 'OrderTree' object has no attribute 'get_price'before.volumeis ever read.This only fires on a populated level.
get_volume_at_pricereaches the bad call solely through theprice_exists(price)guard — when no order rests at that price the guard is false, the function returns the pre-setvolume = 0, and the typo is never exercised. That's why the function looks fine until you query a price that has resting size, which is exactly what a depth read does.Repro. Rest a single bid, then ask for its volume:
(
get_volume_at_price('bid', 100)against an empty book returns0and never crashes, which is what hides the bug.)Fix. Call the method that exists at both sites (
orderbook.py:184and:189):get_price_list(price)returns theOrderListresting at that price (ordertree.py:23-24), andOrderList.volume(orderbook/orderlist.py:15) is the per-level share volume kept in sync on every append/remove — i.e. exactly the numberget_volume_at_priceis meant to return. With this change the call returns the resting depth (e.g.8after two bids of 5 and 3 at the same price) instead of raising; it only fixes which method name is invoked, not what the book holds.Happy to share the failing workload.