nautechsystems/nautilus_trader · error · anyhow::Error

Total quantity unexpectedly zero or negative in average pric

Error message

Total quantity unexpectedly zero or negative in average price calculation: qty={qty}, last_qty={last_qty}, total_qty={total_qty}

What it means

A runtime guard in Position::calculate_avg_px: after the earlier zero checks, the summed total_qty must still be positive before division. Negative inputs that slipped past the non-negative invariant, or floating-point anomalies, would otherwise cause a division by zero or sign-inverted average price, so the method bails with all three quantities in the message.

Source

Thrown at crates/model/src/position.rs:1091

        if qty == 0.0 && last_qty == 0.0 {
            anyhow::bail!("Cannot calculate average price: both quantities are zero");
        }

        if last_qty == 0.0 {
            anyhow::bail!("Cannot calculate average price: fill quantity is zero");
        }

        if qty == 0.0 {
            return Ok(last_px);
        }

        let start_cost = avg_pg * qty;
        let event_cost = last_px * last_qty;
        let total_qty = qty + last_qty;

        // Runtime check to prevent division by zero even in release builds
        if total_qty <= 0.0 {
            anyhow::bail!(
                "Total quantity unexpectedly zero or negative in average price calculation: qty={qty}, last_qty={last_qty}, total_qty={total_qty}"
            );
        }

        Ok((start_cost + event_cost) / total_qty)
    }

    fn calculate_avg_px_open_px(&self, last_px: f64, last_qty: f64) -> f64 {
        self.calculate_avg_px(self.quantity.as_f64(), self.avg_px_open, last_px, last_qty)
            .unwrap_or_else(|e| {
                log::error!("Error calculating average open price: {e}");
                last_px
            })
    }

    fn calculate_avg_px_close_px(&self, last_px: f64, last_qty: f64) -> f64 {
        let Some(avg_px_close) = self.avg_px_close else {
            return last_px;

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Inspect qty, last_qty and total_qty printed in the message to find which input is negative or NaN.
  2. Validate quantities are finite and non-negative at the position/event boundary before applying fills.
  3. Reconcile the position against venue state to repair corrupted quantity data.

Example fix

// before
let avg = position.calculate_avg_px_open_px();
// after
let (q, lq) = (position.quantity().as_f64(), event_qty.as_f64());
if !q.is_finite() || !lq.is_finite() || q < 0.0 || lq < 0.0 {
    anyhow::bail!("invalid quantities for avg px: qty={q}, last_qty={lq}");
}
let avg = position.calculate_avg_px_open_px();
Defensive patterns

Strategy: validation

Validate before calling

fn valid_qty(q: f64) -> bool { q.is_finite() && q > 0.0 }

Type guard

fn is_valid_positive_finite(q: f64) -> bool {
    q.is_finite() && q > 0.0
}

Try / catch

match position.calculate_avg_px_open_px() {
    Ok(px) => px,
    Err(e) if e.to_string().contains("Total quantity unexpectedly") => {
        // reconcile/repair corrupted position state
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling calculate_avg_px_open_px / calculate_avg_px_close_px when qty + last_qty <= 0.0 — only possible with negative quantities that violate the debug_assert invariant, since both inputs are checked >= 0 in debug builds.

Common situations: Corrupted position state with negative quantities (e.g. from buggy external position reconciliation), NaN quantities propagating through arithmetic (NaN comparisons are false, bypassing both guards), or release-build paths where debug assertions are off.

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/51ed1cc4e889e7e7. Report an issue: GitHub.