nautechsystems/nautilus_trader · error

closing quantity was zero

Error message

closing quantity was zero

What it means

split_for_position_flip splits an overfilling OrderFilled into a closing fragment and an opening (flipped position) fragment. The closing quantity must be a positive amount strictly smaller than the fill quantity, otherwise the split is meaningless. This error is thrown when closing_qty is zero, i.e. the caller attempted a position flip with nothing to close — an internal arithmetic/logic error in the position-flip code path.

Source

Thrown at crates/model/src/events/order/filled.rs:168

    #[must_use]
    pub fn is_sell(&self) -> bool {
        self.order_side == OrderSide::Sell
    }

    /// Splits an overfill into the fragment which closes the current position and the
    /// fragment which opens the flipped position.
    ///
    /// # Errors
    ///
    /// Returns an error when `closing_qty` is zero, is not smaller than the fill quantity,
    /// or the proportional commission cannot be represented.
    pub fn split_for_position_flip(
        &self,
        closing_qty: Quantity,
        opening_position_id: Option<PositionId>,
        opening_event_id: UUID4,
    ) -> anyhow::Result<(Self, Self)> {
        anyhow::ensure!(!closing_qty.is_zero(), "closing quantity was zero");
        anyhow::ensure!(
            closing_qty.raw < self.last_qty.raw,
            "closing quantity {closing_qty} must be smaller than fill quantity {}",
            self.last_qty,
        );

        let opening_qty =
            Quantity::from_raw(self.last_qty.raw - closing_qty.raw, closing_qty.precision);
        let closing_fraction = closing_qty.as_decimal() / self.last_qty.as_decimal();
        let (closing_commission, opening_commission) = match self.commission {
            Some(commission) => {
                let closing = Money::from_decimal(
                    commission.as_decimal() * closing_fraction,
                    commission.currency,
                )?;
                (Some(closing), Some(commission - closing))
            }
            None => (None, None),

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Check the current position quantity before flipping; only call flip when the position is non-flat.
  2. Verify the closing quantity computation (fill_qty - position_open_qty) uses the correct signed side logic.
  3. Ensure the Quantity passed was constructed from the correct raw value and precision.
  4. At the call site, skip the split entirely when closing_qty.is_zero() — nothing needs to close.

Example fix

// before
let (closing, opening) = fill.split_for_position_flip(closing_qty, Some(open_id), event_id)?;
// after
if closing_qty.is_zero() {
    // nothing to close: the fill entirely opens/extends the position
    return apply_open_fill(fill);
}
let (closing, opening) = fill.split_for_position_flip(closing_qty, Some(open_id), event_id)?;
Defensive patterns

Strategy: validation

Validate before calling

if closing_qty.is_zero() {
    // nothing to close; handle as a pure open/extend instead of a flip
}
if closing_qty.raw >= fill.last_qty.raw {
    // not an overfill; handle as a normal fill application
}

Type guard

fn is_valid_flip_split(fill_qty: Quantity, closing_qty: Quantity) -> bool {
    !closing_qty.is_zero() && closing_qty.raw < fill_qty.raw
}

Try / catch

match fill.split_for_position_flip(closing_qty, open_id, event_id) {
    Ok((closing, opening)) => { /* apply both fragments */ }
    Err(e) if e.to_string().contains("closing quantity was zero") => {
        // no position to close; apply the fill as open-only
        apply_open_fill(fill)
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling split_for_position_flip (directly or via flip_position / apply_orderless_flip_fill) with a Quantity whose raw value is 0 — typically because the computed close amount against the current position was zero.

Common situations: Calling flip_position when there is no open position to close (position already flat); a signed/unsigned conversion that truncated the closing quantity to zero; an order-size calculation bug where the overfill quantity was computed as fill minus open quantity incorrectly.

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/5b22419a2a66e0fc. Report an issue: GitHub.