nautechsystems/nautilus_trader · error

realized and unrealized PnL currencies differ

Error message

realized and unrealized PnL currencies differ

What it means

try_total_pnl refuses to sum realized_pnl and unrealized PnL when their Money currencies differ. Total PnL must be a single Money value, so mixing e.g. a USD realized PnL with a BTC unrealized PnL is rejected instead of silently converting. Note that a valid currency comparison still proceeds to checked_add (see total PnL overflow for the arithmetic failure).

Source

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

        self.try_calculate_pnl(avg_px_open, avg_px_close, quantity)
            .unwrap_or_else(|e| {
                log::error!("Error calculating PnL: {e}");
                Money::zero(self.settlement_currency)
            })
    }

    /// Returns total P&L (realized + unrealized) based on the last price.
    ///
    /// # Errors
    ///
    /// Returns an error if unrealized P&L cannot be calculated, the realized and unrealized
    /// currencies differ, or the total cannot be represented as [`Money`].
    pub fn try_total_pnl(&self, last: Price) -> anyhow::Result<Money> {
        let unrealized = self.try_unrealized_pnl(last)?;

        match self.realized_pnl {
            Some(realized) => {
                anyhow::ensure!(
                    realized.currency == unrealized.currency,
                    "realized and unrealized PnL currencies differ"
                );
                realized
                    .checked_add(unrealized)
                    .ok_or_else(|| anyhow::anyhow!("total PnL overflow"))
            }
            None => Ok(unrealized),
        }
    }

    /// Returns total P&L (realized + unrealized) based on the last price.
    #[must_use]
    pub fn total_pnl(&self, last: Price) -> Money {
        self.try_total_pnl(last).unwrap_or_else(|e| {
            log::error!("Error calculating total PnL: {e}");
            Money::zero(self.settlement_currency)
        })

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Ensure realized PnL is recorded in the same currency as the position's unrealized PnL currency (instrument's PnL/settlement currency).
  2. Convert one side explicitly at your application layer with a defined FX rate before computing total PnL.
  3. Regenerate position snapshots so realized_pnl currency matches the current instrument configuration.

Example fix

// before: realized in BTC, unrealized in USD -> error
let total = position.try_total_pnl(last_price)?;
// after: normalize realized into the unrealized currency first
let unrealized = position.try_unrealized_pnl(last_price)?;
let realized_converted = convert_fx(position.realized_pnl.unwrap(), unrealized.currency, fx_rate)?;
let total = realized_converted.checked_add(unrealized).ok_or_else(|| anyhow!("total PnL overflow"))?;
Defensive patterns

Strategy: validation

Validate before calling

if let Some(realized) = position.realized_pnl {
    let unrealized = position.try_unrealized_pnl(last)?;
    if realized.currency != unrealized.currency {
        // convert realized into unrealized.currency first
    }
}
let total = position.try_total_pnl(last)?;

Try / catch

match position.try_total_pnl(last) {
    Ok(total) => total,
    Err(e) if e.to_string().contains("currencies differ") => {
        // aggregate components with explicit FX conversion
        let u = position.try_unrealized_pnl(last)?;
        convert_and_add(position.realized_pnl, u)?
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling try_total_pnl (or total_pnl/py_total_pnl) on a position where realized_pnl was accumulated in a different currency than the unrealized PnL currency — typically inverse instruments where realized PnL is in the settlement/base currency but unrealized valuation switched currency, or a position whose realized PnL was set from external aggregation in another currency.

Common situations: Multi-currency or cross-margined instruments where fees were paid in a different currency than PnL; manually constructing or deserializing a Position with realized_pnl in the quote currency while the instrument's PnL currency is the base; changing an instrument's config between sessions so old snapshots disagree with new valuation currency.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


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