{"record":{"id":"caff85a88e3fb732","repo":"nautechsystems/nautilus_trader","slug":"locked-balance-was-negative-locked","errorCode":null,"errorMessage":"locked balance was negative: {locked}","messagePattern":"locked balance was negative: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/model/src/accounts/base.rs","lineNumber":432,"sourceCode":"\n/// Updates the locked balance for the given instrument and currency, then recalculates the\n/// account balance for that currency from all per-(instrument, currency) locks.\n///\n/// The reservation is recorded without a balance when the currency has no observed balance yet,\n/// so a later balance report derives from it.\n///\n/// # Errors\n///\n/// Returns an error if `locked` is negative, its precision differs from the balance precision,\n/// or the reservations cannot produce a valid balance. Balances and reservations are left\n/// unchanged when an error is returned.\npub(crate) fn update_balance_locked(\n    balances: &mut IndexMap<Currency, AccountBalance>,\n    balances_locked: &mut AHashMap<(InstrumentId, Currency), Money>,\n    instrument_id: InstrumentId,\n    locked: Money,\n) -> anyhow::Result<()> {\n    anyhow::ensure!(locked.raw >= 0, \"locked balance was negative: {locked}\");\n\n    let currency = locked.currency;\n    let key = (instrument_id, currency);\n\n    let Some(current_balance) = balances.get(&currency).copied() else {\n        balances_locked.insert(key, locked);\n        return Ok(());\n    };\n\n    anyhow::ensure!(\n        current_balance.currency.precision == currency.precision,\n        \"Cannot update {currency} reservation: precision {} differed from balance precision {}\",\n        currency.precision,\n        current_balance.currency.precision\n    );\n\n    let previous = balances_locked.insert(key, locked);\n","sourceCodeStart":414,"sourceCodeEnd":450,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/model/src/accounts/base.rs#L414-L450","documentation":"`update_balance_locked` reserves a portion of an account balance for a pending order, and a negative locked amount is nonsensical — it would imply releasing more than was reserved or creating balance from nothing. The function rejects the update before mutating any state.","triggerScenarios":"Calling update_balance_locked (or the account's balance-locking path) with a locked Money whose raw value is negative, e.g. when an adapter computes locked balance as a negative delta.","commonSituations":"Adapter balance-sync bugs where held/margin amounts are subtracted twice, sign errors when converting broker 'available' vs 'locked' fields, or test code constructing negative Money.","solutions":["Fix the caller to pass a non-negative locked amount","Check adapter logic that derives locked balance from total vs available so signs are correct","If reducing a reservation, use the release/unlock path instead of passing a negative value"],"exampleFix":"// before\nupdate_balance_locked(balances, locks, instrument_id, Money(-100, USD))\n// after\nupdate_balance_locked(balances, locks, instrument_id, Money(100, USD))","handlingStrategy":"validation","validationCode":"if locked.raw < 0:\n    raise ValueError(f\"cannot lock negative balance: {locked}\")","typeGuard":null,"tryCatchPattern":"try:\n    update_balance_locked(balances, locks, instrument_id, locked)\nexcept Exception as e:\n    log.warning(f\"balance lock rejected: {e}\")  # state unmutated","preventionTips":["Compute locked amounts as absolute values","Use release/unlock paths rather than negative locks","Audit adapters' derivation of held vs available balances"],"tags":["rust","balance","argument-validation"],"backgroundTag":"invalid-argument-value","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"}