nautechsystems/nautilus_trader · error

non-empty fill group

Error message

non-empty fill group

What it means

When synthesizing an `OrderStatusReport` from a group of fills, the live execution manager computes the earliest fill timestamp with `fills.iter().map(...).min().expect("non-empty fill group")`. `Iterator::min` returns `None` for an empty iterator, so the `.expect` panics. The invariant is that every caller passes at least one fill; hitting this panic means that assumption was violated.

Source

Thrown at crates/live/src/execution/manager.rs:1342

                Ok::<_, anyhow::Error>((quantity, notional))
            },
        )?;

        anyhow::ensure!(
            quantity > Decimal::ZERO,
            "fill group quantity is not positive"
        );

        let order_qty = Quantity::from_decimal_dp(quantity, instrument.size_precision())?;
        let avg_px = notional
            .checked_div(quantity)
            .ok_or_else(|| anyhow::anyhow!("fill group average price is not representable"))?;

        let ts_accepted = fills
            .iter()
            .map(|fill| fill.ts_event)
            .min()
            .expect("non-empty fill group");

        let ts_last = fills
            .iter()
            .map(|fill| fill.ts_event)
            .max()
            .expect("non-empty fill group");

        let ts_init = fills
            .iter()
            .map(|fill| fill.ts_init)
            .max()
            .expect("non-empty fill group");

        let report = OrderStatusReport::new(
            first.account_id,
            first.instrument_id,
            first.client_order_id,
            first.venue_order_id,

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Guard the call site: bail or return early when `fills.is_empty()` before constructing the report
  2. Replace the `expect` with `ok_or_else` + `?` so an empty group yields a handled error instead of a panic
  3. Log the instrument_id/client_order_id that produced the empty group to find the upstream filter dropping fills
  4. Review dedup/skip logic in fill-group construction for over-aggressive removal

Example fix

// before
let ts_accepted = fills.iter().map(|f| f.ts_event).min().expect("non-empty fill group");
// after
anyhow::ensure!(!fills.is_empty(), "cannot build OrderStatusReport from empty fill group");
let ts_accepted = fills.iter().map(|f| f.ts_event).min().unwrap();
Defensive patterns

Strategy: type-guard

Validate before calling

if fills.is_empty() {
    return Err(anyhow::anyhow!("empty fill group; cannot build OrderStatusReport"));
}

Type guard

fn non_empty(fills: &[OrderFillReport]) -> Option<&[OrderFillReport]> {
    (!fills.is_empty()).then_some(fills)
}

Try / catch

match result_of_report_build { Ok(r) => r, Err(e) => log::error!("report build failed: {e}") }

Prevention

When it happens

Trigger: Reaching the report-building code with an empty `fills` collection — e.g. an `OrderStatusReport` generated during reconciliation or `generate_order_status_reports` where a matched fill group was filtered/deduplicated down to zero elements.

Common situations: Custom reconciliation logic passing empty fill vectors; an upstream filter removing all fills of a group; a venue status response containing zero fills while the code still attempts report construction.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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