nautechsystems/nautilus_trader · critical

Error: Unable to calculate `spread` (no bid or ask)

Error message

Error: Unable to calculate `spread` (no bid or ask)

What it means

This panic occurs in the FFI wrapper `orderbook_spread` when `OrderBook::spread()` returns None, which happens when the book lacks at least one bid AND one ask, making the spread undefined. Because the wrapper uses `abort_on_panic`, the panic aborts the process instead of returning an error across FFI.

Source

Thrown at crates/model/src/ffi/orderbook/book.rs:308

///
/// Panics if there are no ask orders for best ask size.
#[unsafe(no_mangle)]
#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
pub extern "C" fn orderbook_best_ask_size(book: &mut OrderBook) -> Quantity {
    abort_on_panic(|| {
        book.best_ask_size()
            .expect("Error: No ask orders for best ask size")
    })
}

/// # Panics
///
/// Panics if unable to calculate spread (requires at least one bid and one ask).
#[unsafe(no_mangle)]
pub extern "C" fn orderbook_spread(book: &mut OrderBook) -> f64 {
    abort_on_panic(|| {
        book.spread()
            .expect("Error: Unable to calculate `spread` (no bid or ask)")
    })
}

/// # Panics
///
/// Panics if unable to calculate midpoint (requires at least one bid and one ask).
#[unsafe(no_mangle)]
pub extern "C" fn orderbook_midpoint(book: &mut OrderBook) -> f64 {
    abort_on_panic(|| {
        book.midpoint()
            .expect("Error: Unable to calculate `midpoint` (no bid or ask)")
    })
}

/// # Panics
///
/// Panics if `order_side` is `NoOrderSide`.
#[unsafe(no_mangle)]

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Check that both `has_bid_orders` and `has_ask_orders` (or equivalent best bid/ask availability) hold before calling the spread FFI function.
  2. Wait for a complete initial snapshot (both sides populated) before computing spread.
  3. Compute spread caller-side from optional best bid/ask values, returning None when either is missing.
  4. Fix market-data subscriptions so both bid and ask updates flow for the instrument.

Example fix

// before (Python FFI caller)
spread = orderbook_spread(book)
// after
if has_bid_orders(book) and has_ask_orders(book):
    spread = orderbook_spread(book)
else:
    spread = None
Defensive patterns

Strategy: validation

Validate before calling

def can_compute_spread(book) -> bool:
    return book.bids_len() > 0 and book.asks_len() > 0

Type guard

def is_two_sided(book) -> bool:
    return len(book.bids()) > 0 and len(book.asks()) > 0

Prevention

When it happens

Trigger: Calling `orderbook_spread(book)` when either side (bids or asks) is empty — e.g. one-sided quotes only, before the first full snapshot, or after levels were deleted.

Common situations: Crossed/one-sided books from partial feeds; computing spread during strategy warm-up before data arrives; books for instruments with thin/absent one side; post-disconnect stale books.

Understand the failure class

Background: EmptyResultError / "no results found": when an API or scraper succeeds but returns zero rows — this error's family across 9 libraries.

Related errors


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