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
- Inspect qty, last_qty and total_qty printed in the message to find which input is negative or NaN.
- Validate quantities are finite and non-negative at the position/event boundary before applying fills.
- 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
- Validate fills are finite and positive before applying them to positions.
- Periodically reconcile position quantities against venue data to catch corruption early.
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
- Cannot calculate average price: both quantities are zero
- Cannot calculate average price: fill quantity is zero
- Cannot persist position with no events: {}
- Close command should not be drained
- Flush command should not be drained
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/51ed1cc4e889e7e7.
Report an issue: GitHub.