nautechsystems/nautilus_trader · error

invalid OrderSide: must be Buy or Sell, was {side}

Error message

invalid OrderSide: must be Buy or Sell, was {side}

What it means

specified_order_side (crates/model/src/data/bet.rs:778) narrows the three-valued OrderSide (BUY/SELL/NO_ORDER_SIDE) into OrderSideSpecified for the betting conversion functions (probability_to_bet, inverse_probability_to_bet). NO_ORDER_SIDE means "unset", so rather than guessing a direction the function bails.

Source

Thrown at crates/model/src/data/bet.rs:786

fn check_nonzero_denominator(value: Decimal, name: &str) -> anyhow::Result<()> {
    if value.is_zero() {
        anyhow::bail!("invalid {name}: must be non-zero")
    }
    Ok(())
}

/// Converts [`OrderSide`] into a specified side for betting conversions.
///
/// # Errors
///
/// Returns an error if `side` is [`OrderSide::NoOrderSide`].
pub fn specified_order_side(side: OrderSide) -> anyhow::Result<OrderSideSpecified> {
    match side {
        OrderSide::Buy => Ok(OrderSideSpecified::Buy),
        OrderSide::Sell => Ok(OrderSideSpecified::Sell),
        OrderSide::NoOrderSide => {
            anyhow::bail!("invalid OrderSide: must be Buy or Sell, was {side}")
        }
    }
}

fn checked_add(lhs: Decimal, rhs: Decimal) -> anyhow::Result<Decimal> {
    lhs.checked_add(rhs)
        .ok_or_else(|| anyhow::anyhow!("Decimal overflow adding {lhs} and {rhs}"))
}

fn checked_sub(lhs: Decimal, rhs: Decimal) -> anyhow::Result<Decimal> {
    lhs.checked_sub(rhs)
        .ok_or_else(|| anyhow::anyhow!("Decimal overflow subtracting {rhs} from {lhs}"))
}

fn checked_mul(lhs: Decimal, rhs: Decimal) -> anyhow::Result<Decimal> {
    lhs.checked_mul(rhs)
        .ok_or_else(|| anyhow::anyhow!("Decimal overflow multiplying {lhs} by {rhs}"))
}

View on GitHub (pinned to 2114cf6f76)

Solutions

  1. Set the order side (BUY or SELL) before any betting conversion.
  2. Filter or skip orders with OrderSide::NoOrderSide before converting.
  3. Fix the upstream construction or deserialization that leaves the side unset.

Example fix

// before
let bet = probability_to_bet(prob, volume, specified_order_side(order.side())?)?;

// after
let side = match order.side() {
    OrderSide::Buy => OrderSideSpecified::Buy,
    OrderSide::Sell => OrderSideSpecified::Sell,
    OrderSide::NoOrderSide => {
        anyhow::bail!("order {} has no side; set BUY or SELL first", order.id())
    }
};
let bet = probability_to_bet(prob, volume, side)?;
Defensive patterns

Strategy: type-guard

Validate before calling

if matches!(order.side(), OrderSide::NoOrderSide) {
    anyhow::bail!("order side is unset; set BUY or SELL before betting conversion");
}
let side = specified_order_side(order.side())?;

Type guard

fn has_specified_side(side: OrderSide) -> bool {
    !matches!(side, OrderSide::NoOrderSide)
}

Try / catch

match specified_order_side(order.side()) {
    Ok(side) => side,
    Err(e) if e.to_string().contains("must be Buy or Sell") => {
        anyhow::bail!("order {} has no side set", order.client_order_id())
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: specified_order_side(OrderSide::NoOrderSide), usually indirectly: passing an order whose side was never set into probability_to_bet/inverse_probability_to_bet, or a default-initialized OrderSide enum value.

Common situations: Constructing orders without a side; deserialization falling back to the NO_ORDER_SIDE default on missing fields; generic code that converts every order in a collection when some are sideless placeholders.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@2114cf6f76 (2026-08-21). Data as JSON: /api/errors/b70cc8a91d5b58d6. Report an issue: GitHub.