nautechsystems/nautilus_trader · error

BookOrder side must be Buy or Sell

Error message

BookOrder side must be Buy or Sell

What it means

BookOrder::to_book_price() converts the order's price into a BookPrice, but BookOrder.side is an Option<OrderSide>. The library panics via expect when side is None because a book order without a side has no valid price interpretation. It is a deliberate fail-fast for a struct that was constructed incompletely.

Source

Thrown at crates/model/src/data/order.rs:96

    ) -> Self {
        Self {
            side: side.into(),
            price,
            size,
            order_id,
        }
    }

    /// Returns a [`BookPrice`] from this order.
    ///
    /// # Panics
    ///
    /// Panics if `self.side` is `None`.
    #[must_use]
    pub fn to_book_price(&self) -> BookPrice {
        BookPrice::new(
            self.price,
            self.side.expect("BookOrder side must be Buy or Sell"),
        )
    }

    /// Returns the order exposure as an `f64`.
    #[must_use]
    pub fn exposure(&self) -> f64 {
        self.price.as_f64() * self.size.as_f64()
    }

    /// Returns the signed order size as `f64`, positive for buys, negative for sells.
    ///
    /// # Panics
    ///
    /// Panics if `self.side` is `None`.
    #[must_use]
    pub fn signed_size(&self) -> f64 {
        match self.side {
            Some(OrderSide::Buy) => self.size.as_f64(),

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Ensure the BookOrder is constructed with a valid Some(OrderSide::Buy) or Some(OrderSide::Sell) before calling to_book_price().
  2. Check side.is_some() before calling to_book_price(), or match on the Option and handle the None case explicitly.
  3. If the order came from an upstream feed, fix the parser/adapter so the side is always populated from the message.

Example fix

// before
let book_price = order.to_book_price();
// after
let book_price = order
    .side
    .map(|_| order.to_book_price())
    .unwrap_or_else(|| panic!("BookOrder {:?} has no side; skipping", order.order_id));
Defensive patterns

Strategy: type-guard

Validate before calling

if order.side.is_none() { /* skip or reconstruct from feed */ }

Type guard

fn has_side(order: &BookOrder) -> bool { order.side.is_some() }

Try / catch

// Rust panics are not catchable idiomatic; guard instead:
let price = order.side.map(|_| order.to_book_price());

Prevention

When it happens

Trigger: Calling to_book_price() on a BookOrder whose side field is None — typically a BookOrder built with side: None (e.g. from a partial L1/L2 update that did not carry a side, or a default-constructed order) then passed into book assembly paths like add, replace_l1, or book integrity checks.

Common situations: Feeding market-data deltas where an L1 update lacks a side; constructing BookOrder manually for tests with side omitted; deserializing a partial order book snapshot that dropped the side field.

Related errors


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