{"record":{"id":"1dbc2788d88512db","repo":"nautechsystems/nautilus_trader","slug":"failed-to-parse-liab-liab-str-e","errorCode":null,"errorMessage":"Failed to parse liab '{liab_str}': {e}","messagePattern":"Failed to parse liab '(.+?)': (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/adapters/okx/src/common/parse.rs","lineNumber":974,"sourceCode":"    account_id: AccountId,\n    instrument_id: InstrumentId,\n    size_precision: u8,\n    ts_init: UnixNanos,\n) -> anyhow::Result<Option<PositionStatusReport>> {\n    // OKX returns empty strings for zero values, normalize to \"0\" before parsing\n    let liab_str = if balance.liab.trim().is_empty() {\n        \"0\"\n    } else {\n        balance.liab.trim()\n    };\n    let spot_in_use_str = if balance.spot_in_use_amt.trim().is_empty() {\n        \"0\"\n    } else {\n        balance.spot_in_use_amt.trim()\n    };\n\n    let liab_dec = Decimal::from_str(liab_str)\n        .map_err(|e| anyhow::anyhow!(\"Failed to parse liab '{liab_str}': {e}\"))?;\n    let spot_in_use_dec = Decimal::from_str(spot_in_use_str)\n        .map_err(|e| anyhow::anyhow!(\"Failed to parse spotInUseAmt '{spot_in_use_str}': {e}\"))?;\n\n    // Skip if no margin position (no liability and no spot in use)\n    if liab_dec.is_zero() && spot_in_use_dec.is_zero() {\n        return Ok(None);\n    }\n\n    // Check if spotInUseAmt is zero first\n    if spot_in_use_dec.is_zero() {\n        // No position if spotInUseAmt is zero (regardless of liability)\n        return Ok(None);\n    }\n\n    // Position side based on spotInUseAmt sign\n    let (position_side, quantity_dec) = if spot_in_use_dec.is_sign_negative() {\n        // Negative spotInUseAmt = sold (short position)\n        (PositionSide::Short, spot_in_use_dec.abs())","sourceCodeStart":956,"sourceCodeEnd":992,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/adapters/okx/src/common/parse.rs#L956-L992","documentation":"parse_spot_margin_position_from_balance parses the OKX balance liability field (liab, or \"0\" when absent) into a Decimal. This error is raised when the liab string is not a valid decimal number, so the spot margin position cannot be derived from the balance record.","triggerScenarios":"balance.liab (or the liability currency field) contains an empty, non-numeric, or malformed string (e.g. \"\", \"NaN\", thousands separators) when parsing an OKX spot margin balance response.","commonSituations":"OKX returning empty liab for currencies with no position but unexpected formats; API schema changes; mocked/recorded fixtures with placeholder values; locale-formatted numbers from custom middleware.","solutions":["Log the raw liab string from the balance payload for the failing currency","Treat empty or missing liab as \"0\" before calling the parser (the code already defaults None to \"0\"; check empty strings too)","Normalize/sanitize the balance record upstream (trim, strip separators) before parsing","Upgrade the OKX adapter if a newer version handles the field format"],"exampleFix":"// before\nlet liab_dec = Decimal::from_str(liab_str).map_err(|e| anyhow!(\"Failed to parse liab '{liab_str}': {e}\"))?;\n// after\nlet liab_str = liab_str.trim();\nlet liab_dec = if liab_str.is_empty() { Decimal::ZERO } else { Decimal::from_str(liab_str).map_err(|e| anyhow!(\"Failed to parse liab '{liab_str}': {e}\"))? };","handlingStrategy":"validation","validationCode":"let liab = balance.liab.as_deref().unwrap_or(\"0\").trim();\nif liab.is_empty() || rust_decimal::Decimal::from_str(liab).is_err() { /* default to \"0\" or skip record */ }","typeGuard":"fn parseable_decimal(s: Option<&str>) -> bool {\n    s.unwrap_or(\"0\").trim().pipe(|t| !t.is_empty() && rust_decimal::Decimal::from_str(t).is_ok())\n}","tryCatchPattern":"match parse_spot_margin_position_from_balance(&balance, ...) {\n    Ok(Some(pos)) => handle(pos),\n    Ok(None) => {},\n    Err(e) if e.to_string().contains(\"Failed to parse liab\") => log::warn!(\"bad liab '{}': {e}\", balance.liab),\n    Err(e) => return Err(e),\n}","preventionTips":["Normalize empty/missing numeric fields to \"0\" before parsing","Validate all balance numeric strings after any OKX API version bump","Sanitize recorded fixtures to realistic values","Add unit tests for empty-string balance fields"],"tags":["okx","rust","parsing","decimal","margin"],"backgroundTag":"invalid-argument-format","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"}