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
- Ensure realized PnL is recorded in the same currency as the position's unrealized PnL currency (instrument's PnL/settlement currency).
- Convert one side explicitly at your application layer with a defined FX rate before computing total PnL.
- 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
- Record realized PnL in the instrument's PnL/settlement currency.
- Normalize multi-currency fees to the PnL currency when they occur.
- Keep instrument currency configuration stable across sessions.
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
- total PnL overflow
- Cannot calculate inverse points: open price is not positive
- Cannot calculate inverse points: close price is not positive
- position fill void exceeds known fragments for {}
- stale position fill void for {}
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/3cfc17cf30508bb9.
Report an issue: GitHub.