nautechsystems/nautilus_trader · error

Cannot batch modify empty order list

Error message

Cannot batch modify empty order list

What it means

`modify_orders` refuses to build a BatchModifyOrders command when the `updates` list is empty. A batch request must contain at least one (order, update) pair; an empty list would produce a meaningless or invalid command for the execution engine. The bail happens before any trader/strategy IDs are resolved.

Source

Thrown at crates/trading/src/strategy/mod.rs:464

    /// Batch modifies multiple orders for the same instrument.
    ///
    /// Each tuple is `(client_order_id, quantity, price, trigger_price)`.
    ///
    /// # Errors
    ///
    /// Returns an error if the strategy is not registered, the orders span multiple instruments,
    /// contain emulated/local orders, or a child modify is invalid.
    fn modify_orders(
        &mut self,
        updates: Vec<BatchModifyOrder>,
        client_id: Option<ClientId>,
        params: Option<Params>,
    ) -> anyhow::Result<()>
    where
        Self: StrategyNative,
    {
        if updates.is_empty() {
            anyhow::bail!("Cannot batch modify empty order list");
        }

        let (trader_id, strategy_id, ts_init) = {
            let core = StrategyNative::strategy_core_mut(self);
            (
                registered_trader_id(core)?,
                registered_strategy_id(core)?,
                core.clock_mut().timestamp_ns(),
            )
        };

        let orders: Vec<OrderAny> = {
            let cache_rc = StrategyNative::strategy_core_mut(self).cache_rc();
            let cache = cache_rc.borrow();
            updates
                .iter()
                .map(|(client_order_id, _, _, _)| {
                    cache

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Ensure the updates list is non-empty before calling modify_orders
  2. Guard the call: if updates.is_empty() { return Ok(()); } (skip instead of error)
  3. Check upstream logic that filters/derives the update list so it cannot drop all entries
  4. If an empty batch is legitimately a no-op, handle it at the call site rather than invoking the API

Example fix

// before
strategy.modify_orders(updates, instrument_id, None, None).await?;
// after
if !updates.is_empty() {
    strategy.modify_orders(updates, instrument_id, None, None).await?;
}
Defensive patterns

Strategy: validation

Validate before calling

if updates.is_empty() {
    // no-op or return early; do not call modify_orders
    return Ok(());
}

Type guard

fn is_non_empty<T>(items: &[T]) -> bool { !items.is_empty() }

Prevention

When it happens

Trigger: Calling `strategy.modify_orders(vec![], instrument_id, None, None)` (or an updates slice built by filtering an original list down to zero elements).

Common situations: Filtering pending updates before the call (e.g. skipping already-closed orders) so the vec becomes empty; upstream data returning no actionable updates; uninitialized/default vectors passed by mistake.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


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