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

  1. Inspect the BookOrder being passed to add and ensure `side` is Some(OrderSide::Buy) or Some(OrderSide::Sell) before constructing it.
  2. 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.
  3. 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.
  4. 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`.
  5. 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

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


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/a7a39b7f4c9be2e5. Report an issue: GitHub.