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
- Check `order.side.is_some()` (or match on it) before calling `signed_size()` and skip `None`-sided orders.
- Use `match order.side` to handle Buy/Sell/None explicitly instead of relying on the panicking helper.
- 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
- Filter NULL/clear orders (side 'N' in MBO feeds) before signed-size aggregation
- Always initialize BookOrder.side when constructing orders manually
- Treat side-less orders as book-maintenance events, not size-bearing orders
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
- `handle_book_mbo` {IMPL_ERR} `{}`
- MBO delta tail must be queued
- L3 queue update should be processed
- BookOrder side must be Buy or Sell
- in-flight mutex poisoned
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/e2faad48917ae699.
Report an issue: GitHub.