nautechsystems/nautilus_trader · critical · BookIntegrityError

NoOrderSide

Error message

NoOrderSide

What it means

This panic comes from `BookOrder::signed_size()`, which returns size as a sign-carrying f64: positive for `OrderSide::Buy`, negative for `OrderSide::Sell`. If `self.side` is `None` the order has no directional side (e.g. a NULL/clear book order), the sign is undefined, and the method panics with `BookIntegrityError::NoOrderSide` rather than returning a misleading zero.

Source

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

    }

    /// 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(),
            Some(OrderSide::Sell) => -(self.size.as_f64()),
            None => panic!("{}", BookIntegrityError::NoOrderSide),
        }
    }
}

impl Default for BookOrder {
    /// Creates a NULL [`BookOrder`] instance.
    fn default() -> Self {
        NULL_ORDER
    }
}

impl PartialEq for BookOrder {
    fn eq(&self, other: &Self) -> bool {
        self.order_id == other.order_id
    }
}

impl Hash for BookOrder {

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Check `order.side.is_some()` (or match on it) before calling `signed_size()` and skip `None`-sided orders.
  2. Use `match order.side` to handle Buy/Sell/None explicitly instead of relying on the panicking helper.
  3. Filter out NULL/clear placeholder orders when aggregating book size so only real orders contribute.

Example fix

// before
let signed = order.signed_size();
// after
let signed = match order.side {
    Some(OrderSide::Buy) => order.size.as_f64(),
    Some(OrderSide::Sell) => -order.size.as_f64(),
    None => 0.0, // skip NULL / clear orders
};
Defensive patterns

Strategy: type-guard

Validate before calling

if order.side.is_none() {
    // skip NULL / clear placeholder orders
    return 0.0;
}

Type guard

fn real_order(order: &BookOrder) -> bool {
    matches!(order.side, Some(OrderSide::Buy) | Some(OrderSide::Sell))
}

Try / catch

// signed_size panics; avoid catching by matching side explicitly
let signed = match order.side {
    Some(side) => signed_size_for(side, order.size.as_f64()),
    None => 0.0,
};

Prevention

When it happens

Trigger: Calling `order.signed_size()` on a `BookOrder` whose `side` field is `None` — typically a NULL-order sentinel, a book-clear/delete action decoded with side 'N', or an order struct constructed without setting `side`. Exercised by `test_signed_size` (valid sides) and exposed to Python via `py_signed_size`.

Common situations: Iterating all orders in a `Book` (which contains NULL placeholder orders) and computing signed aggregate size without filtering; handling Databento MBO Clear actions whose side is 'N'; deserializing order snapshots where side was omitted.

Related errors


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