{"record":{"id":"3cfc17cf30508bb9","repo":"nautechsystems/nautilus_trader","slug":"realized-and-unrealized-pnl-currencies-differ","errorCode":null,"errorMessage":"realized and unrealized PnL currencies differ","messagePattern":"realized and unrealized PnL currencies differ","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/model/src/position.rs","lineNumber":1225,"sourceCode":"        self.try_calculate_pnl(avg_px_open, avg_px_close, quantity)\n            .unwrap_or_else(|e| {\n                log::error!(\"Error calculating PnL: {e}\");\n                Money::zero(self.settlement_currency)\n            })\n    }\n\n    /// Returns total P&L (realized + unrealized) based on the last price.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if unrealized P&L cannot be calculated, the realized and unrealized\n    /// currencies differ, or the total cannot be represented as [`Money`].\n    pub fn try_total_pnl(&self, last: Price) -> anyhow::Result<Money> {\n        let unrealized = self.try_unrealized_pnl(last)?;\n\n        match self.realized_pnl {\n            Some(realized) => {\n                anyhow::ensure!(\n                    realized.currency == unrealized.currency,\n                    \"realized and unrealized PnL currencies differ\"\n                );\n                realized\n                    .checked_add(unrealized)\n                    .ok_or_else(|| anyhow::anyhow!(\"total PnL overflow\"))\n            }\n            None => Ok(unrealized),\n        }\n    }\n\n    /// Returns total P&L (realized + unrealized) based on the last price.\n    #[must_use]\n    pub fn total_pnl(&self, last: Price) -> Money {\n        self.try_total_pnl(last).unwrap_or_else(|e| {\n            log::error!(\"Error calculating total PnL: {e}\");\n            Money::zero(self.settlement_currency)\n        })","sourceCodeStart":1207,"sourceCodeEnd":1243,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/model/src/position.rs#L1207-L1243","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: realized in BTC, unrealized in USD -> error\nlet total = position.try_total_pnl(last_price)?;\n// after: normalize realized into the unrealized currency first\nlet unrealized = position.try_unrealized_pnl(last_price)?;\nlet realized_converted = convert_fx(position.realized_pnl.unwrap(), unrealized.currency, fx_rate)?;\nlet total = realized_converted.checked_add(unrealized).ok_or_else(|| anyhow!(\"total PnL overflow\"))?;","handlingStrategy":"validation","validationCode":"if let Some(realized) = position.realized_pnl {\n    let unrealized = position.try_unrealized_pnl(last)?;\n    if realized.currency != unrealized.currency {\n        // convert realized into unrealized.currency first\n    }\n}\nlet total = position.try_total_pnl(last)?;","typeGuard":null,"tryCatchPattern":"match position.try_total_pnl(last) {\n    Ok(total) => total,\n    Err(e) if e.to_string().contains(\"currencies differ\") => {\n        // aggregate components with explicit FX conversion\n        let u = position.try_unrealized_pnl(last)?;\n        convert_and_add(position.realized_pnl, u)?\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["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."],"tags":["position","pnl","currency","money","rust"],"backgroundTag":"type-mismatch","analyzedSha":"18893faf8b356be3320add8de2f861b0b647cf06","analyzedAt":"2026-09-08T20:49:34.690Z","contentChangedAt":"2026-09-08T20:49:34.690Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}