nautechsystems/nautilus_trader · error · anyhow::Error

fill group quantity is not positive

Error message

fill group quantity is not positive

What it means

After aggregating a fill group, the manager asserts via `anyhow::ensure!` that the summed fill quantity is strictly positive (`quantity > Decimal::ZERO`). This guards the invariant that a fill group representing an executed order must have positive total size; it is thrown when the sum is zero or negative. Zero-size or negative-quantity fills indicate malformed fill data, cancellation-as-fill handling bugs, or wrong-side quantity signs from the venue adapter.

Source

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

                let quantity = quantity.checked_add(fill_quantity).ok_or_else(|| {
                    anyhow::anyhow!("fill quantity overflow while aggregating fill group")
                })?;

                let fill_notional = fill_quantity
                    .checked_mul(fill.last_px.as_decimal())
                    .ok_or_else(|| {
                        anyhow::anyhow!("fill notional overflow while aggregating fill group")
                    })?;

                let notional = notional.checked_add(fill_notional).ok_or_else(|| {
                    anyhow::anyhow!("fill notional overflow while aggregating fill group")
                })?;

                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)

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Inspect the fills in the group and their `last_qty` signs and values
  2. Fix the adapter to report fill quantities as positive values regardless of reduce/long/short direction
  3. Filter out zero-quantity or non-fill (cancel/expire/reject) events before the fill-group aggregation path
  4. Verify the instrument's quantity handling matches venue semantics (e.g. contracts vs base units)
  5. Enable debug logging of raw venue fill messages and report the adapter bug if quantities look valid

Example fix

// before: venue reports reduction fills with negative qty
let qty = raw_qty;
// after: normalize to positive; side is carried by the order
let qty = raw_qty.abs();
Defensive patterns

Strategy: validation

Validate before calling

fn fill_group_has_positive_quantity(fills: &[Fill]) -> bool {
    !fills.is_empty()
        && fills.iter().fold(Decimal::ZERO, |acc, f| acc + f.last_qty.as_decimal())
            > Decimal::ZERO
}

Type guard

fn is_positive_fill(f: &Fill) -> bool {
    f.last_qty.as_decimal() > Decimal::ZERO
}

Try / catch

match manager.aggregate_fill_group(&fills) {
    Ok(r) => handle(r),
    Err(e) if e.to_string().contains("quantity is not positive") => {
        log::error!("zero/negative aggregate fill quantity: {e}");
        discard_or_reconcile_group(&fills);
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Processing a fill group where every fill has `last_qty == 0`, or where venue-reported quantities carry negative signs (some venues report position reductions as negative quantities) so the aggregate lands at or below zero; also possible when the fills list contains cancel/reject/expire records misrouted into the fill aggregation path.

Common situations: Exchange adapter not converting venue-side signed quantities to positive `last_qty` with a separate order-side; adapter emitting zero-quantity fill events for expired/canceled orders; replaying corrupted or truncated fill logs; reconciliation logic including reject/cancel events as fills.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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