nautechsystems/nautilus_trader · critical
BookOrder side must be Buy or Sell
Error message
BookOrder side must be Buy or Sell
What it means
OrderBook::add unwraps the order's `side` field with expect and panics with this message when it is None. BookOrder models side as an Option<OrderSide> so it can round-trip feed data where the side is missing or unparseable; the book ladder (bids/asks) cannot route an order without a side, so the library treats a None side as a hard programming/data error and panics instead of silently dropping the update.
Source
Thrown at crates/model/src/orderbook/book.rs:130
}
/// Resets the order book to its initial empty state.
pub fn reset(&mut self) {
self.bids.clear();
self.asks.clear();
self.sequence = 0;
self.ts_last = UnixNanos::default();
self.update_count = 0;
}
/// Adds an order to the book after preprocessing based on book type.
///
/// # Panics
///
/// Panics if `order.side` is `None`.
pub fn add(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
let order = pre_process_order(self.book_type, order, flags);
match order.side.expect("BookOrder side must be Buy or Sell") {
OrderSide::Buy => self.bids.add(order, flags),
OrderSide::Sell => self.asks.add(order, flags),
}
self.increment(sequence, ts_event, flags);
}
/// Updates an existing order in the book after preprocessing based on book type.
///
/// # Panics
///
/// Panics if `order.side` is `None`.
pub fn update(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
let order = pre_process_order(self.book_type, order, flags);
match order.side.expect("BookOrder side must be Buy or Sell") {
OrderSide::Buy => self.bids.update(order, flags),
OrderSide::Sell => self.asks.update(order, flags),
}View on GitHub (pinned to 18893faf8b)
Solutions
- Inspect the BookOrder being passed to add and ensure `side` is Some(OrderSide::Buy) or Some(OrderSide::Sell) before constructing it.
- In the feed adapter/parser, map every raw side value explicitly to Buy/Sell and skip (with a warning) records that have no valid side instead of building a BookOrder with side None.
- Log the offending raw record (price, qty, sequence) at the parser level before constructing BookOrder so the source of the None side can be identified.
- If side can legitimately be missing for your book type, use an L1/L2 aggregation path that tolerates it, or guard the call site with `if let Some(side) = order.side`.
- Upgrade to the latest nautilus_model version — check release notes in case side handling for your venue adapter changed.
Example fix
// before
let order = BookOrder::new(side_from_feed, price, size, order_id); // side_from_feed: Option<OrderSide>
book.add(order, flags, sequence, ts_event);
// after
match side_from_feed {
Some(side @ (OrderSide::Buy | OrderSide::Sell)) => {
book.add(BookOrder::new(side, price, size, order_id), flags, sequence, ts_event);
}
None => log::warn!("skipping depth record with missing side, seq={sequence}"),
} Defensive patterns
Strategy: validation
Validate before calling
if order.side.is_none() {
log::warn!("skip book add: missing side for id={:?}", order.order_id);
return;
}
book.add(order, flags, sequence, ts_event); Type guard
fn has_side(order: &BookOrder) -> bool {
matches!(order.side, Some(OrderSide::Buy | OrderSide::Sell))
} Try / catch
// Rust panics are not catchable here; validate before calling. In Python bindings, wrap in try/except BaseException only for research tooling:
try:
book.add(order, flags, sequence, ts_event)
except BaseException as e:
log.warning("book add panicked: %s", e) Prevention
- Never construct BookOrder with side None; make the feed->BookOrder mapping total (all enum values handled).
- Skip-and-log invalid feed records at the parser boundary.
- Add unit tests for adapter side mapping including unknown values.
- Assert side presence where deltas are decoded, not deep inside book code.
When it happens
Trigger: Calling OrderBook::add (directly, from Python via py_add, or indirectly through apply_delta / snapshot parsing adapters like parse_book_snapshot_response, parse_order_book, parse_l2_book_snapshot) with a BookOrder whose `side` is None — typically constructed from feed data where the side field was absent or did not map to Buy/Sell.
Common situations: Custom adapter code building BookOrder from a vendor feed that emits unknown/blank side values; a new venue whose side mapping isn't handled in the parser; L1/L2/L3 preprocessing (pre_process_order) does not fill in a missing side, so any None survives to the expect.
Related errors
- {e}
- Slice length != 10
- Error: No bid orders for best bid price
- Error: No ask orders for best ask price
- Error: No bid orders for best bid size
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/a7a39b7f4c9be2e5.
Report an issue: GitHub.