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_id

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Ensure the first fill sets avg_px_open correctly before return/PnL is calculated.
  2. Check the position lifecycle: only calculate_return on positions that actually opened.
  3. Validate fill prices > 0 at the strategy/adapter boundary.
  4. 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

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


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