nautechsystems/nautilus_trader · error · anyhow::Error
Cannot calculate return: open price is zero (close price: {a
Error message
Cannot calculate return: open price is zero (close price: {avg_px_close}) What it means
calculate_return computes (points between open and close) / avg_px_open, so a zero open price would divide by zero. The method explicitly bails with this message when avg_px_open == 0.0, including the close price in the message for debugging.
Source
Thrown at crates/model/src/position.rs:1160
anyhow::bail!(
"Cannot calculate inverse points: close price is not positive or is too small ({avg_px_close})"
);
}
let inverse_open = 1.0 / avg_px_open;
let inverse_close = 1.0 / avg_px_close;
let result = match self.side {
PositionSide::Long => inverse_open - inverse_close,
PositionSide::Short => inverse_close - inverse_open,
PositionSide::Flat => 0.0,
};
Ok(result)
}
fn calculate_return(&self, avg_px_open: f64, avg_px_close: f64) -> anyhow::Result<f64> {
// Prevent division by zero in return calculation
if avg_px_open == 0.0 {
anyhow::bail!(
"Cannot calculate return: open price is zero (close price: {avg_px_close})"
);
}
Ok(self.calculate_points(avg_px_open, avg_px_close) / avg_px_open)
}
fn calculate_pnl_raw(
&self,
avg_px_open: f64,
avg_px_close: f64,
quantity: f64,
) -> anyhow::Result<f64> {
let quantity = quantity.min(self.signed_qty.abs());
let result = if self.is_inverse {
anyhow::ensure!(
self.base_currency.is_some(),
"inverse position {} has no base currency",
self.instrument_idView on GitHub (pinned to 18893faf8b)
Solutions
- Ensure the first fill sets avg_px_open correctly before return/PnL is calculated.
- Check the position lifecycle: only calculate_return on positions that actually opened.
- Validate fill prices > 0 at the strategy/adapter boundary.
- Log and inspect the close price in the message to trace where the zero originated.
Example fix
// before
let ret = position.calculate_return(avg_px_open, avg_px_close).unwrap();
// after
let ret = if avg_px_open != 0.0 { position.calculate_return(avg_px_open, avg_px_close).unwrap() } else { 0.0 }; Defensive patterns
Strategy: validation
Validate before calling
// Rust: skip return calc for positions that never opened
if avg_px_open == 0.0 {
log::debug!("position not opened; return = 0");
return Ok(0.0);
} Try / catch
let ret = position.calculate_return(avg_px_open, avg_px_close)
.unwrap_or_else(|e| { log::warn!("return calc failed: {e}"); 0.0 }); Prevention
- Enforce position lifecycle ordering: open fill before PnL/return.
- Reject zero-price fills at the strategy boundary.
- Check adapter fill-price mapping when migrating venues.
- Add an invariant check that every filled position has avg_px_open > 0.
When it happens
Trigger: handle_buy_order_fill or handle_sell_order_fill on a Position whose avg_px_open is 0.0 — i.e. a fill processed before the position's open price was initialized, or a position constructed with a zero entry price.
Common situations: Flat-position accounting bugs where return is computed for a position that never opened; zero-price fills from a broken venue adapter; deserialized positions missing their open price.
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
- Cannot convert SHORT position from quote to base: avg_px is
- Cannot update position {}: not found in cache
- Cannot update position {position_id}: not found in cache
- Duplicate position fill
- Cannot calculate price impact, the spot price before is not
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/97fd7bc1e737f58f.
Report an issue: GitHub.