nautechsystems/nautilus_trader · error

Order side must be Buy or Sell

Error message

Order side must be Buy or Sell

What it means

The FFI constructor `BookPrice` (in crates/model/src/ffi/orderbook/level.rs) panics when the `order_side` argument cannot be decoded into a Buy/Sell side via `as_option()`. A BookLevel must be tagged with a valid side to know whether to aggregate bid or ask orders, so an invalid side aborts construction. This guards the C ABI against invalid enum input.

Source

Thrown at crates/model/src/ffi/orderbook/level.rs:50

///
/// Panics if `order_side` is `NoOrderSide`.
///
/// Returns an owning pointer to the heap-allocated `BookLevel` which the caller must
/// eventually pass to [`level_drop`].
pub unsafe extern "C" fn level_new(
    order_side: OrderSideOptional,
    price: Price,
    orders: CVec,
) -> *mut BookLevel {
    let orders = unsafe { orders.into_vec::<BookOrderFfi>() }
        .into_iter()
        .map(Into::into)
        .collect::<Vec<BookOrder>>();
    let price = BookPrice {
        value: price,
        side: order_side
            .as_option()
            .expect("Order side must be Buy or Sell"),
    };
    let mut level = BookLevel::new(price);
    level.add_bulk(&orders);
    Box::into_raw(Box::new(level))
}

/// # Safety
///
/// `level` must be a live owning pointer returned by [`level_new`] or [`level_clone`],
/// and must not be used after this call.
///
/// # Panics
///
/// Panics if `level` is null.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn level_drop(level: *mut BookLevel) {
    abort_on_panic(|| {
        assert!(!level.is_null(), "`level` was NULL");

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Pass a concrete OrderSide (BUY or SELL) when constructing the level.
  2. Validate the side value in the caller language before crossing the FFI boundary.
  3. Inspect the call site for a variable that was never assigned a side (defaulted to None).
  4. Rebuild generated bindings if the enum discriminants changed between versions.

Example fix

// before
level = BookPrice(price, None, orders)
// after
side = OrderSide.BUY  # or OrderSide.SELL
level = BookPrice(price, side, orders)
Defensive patterns

Strategy: validation

Validate before calling

# Python caller
assert side in (OrderSide.BUY, OrderSide.SELL), f"invalid side: {side}"
price_ptr = BookPrice(price, side, orders)

Type guard

def is_valid_side(side) -> bool:
    return side in (OrderSide.BUY, OrderSide.SELL)

Prevention

When it happens

Trigger: Calling the exported `BookPrice` FFI function with a null/invalid order_side handle so `OrderSide::as_option()` returns None while building the BookPrice struct.

Common situations: Bindings passing Python None for side, integer side values outside 1..2, or uninitialized FFI struct memory when constructing a book level from raw orders.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


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