{"record":{"id":"be163252c7c5f0d0","repo":"nautechsystems/nautilus_trader","slug":"failed-to-parse-spotinuseamt-spot-in-use-str","errorCode":null,"errorMessage":"Failed to parse spotInUseAmt '{spot_in_use_str}': {e}","messagePattern":"Failed to parse spotInUseAmt '(.+?)': (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/adapters/okx/src/common/parse.rs","lineNumber":976,"sourceCode":"    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())\n    } else {\n        // Positive spotInUseAmt = bought (long position)","sourceCodeStart":958,"sourceCodeEnd":994,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/adapters/okx/src/common/parse.rs#L958-L994","documentation":"The sibling of the liab parse: parse_spot_margin_position_from_balance parses spot_in_use_amt (trimmed) into a Decimal to detect whether a spot margin position exists. This error is raised when spot_in_use_amt is not a valid decimal number.","triggerScenarios":"balance.spot_in_use_amt contains an empty, malformed, or non-numeric string in an OKX spot margin balance record, after trimming.","commonSituations":"Empty strings returned by OKX for currencies without margin usage; API response schema changes; fixtures with placeholder values; custom middleware altering number formatting.","solutions":["Log the raw spot_in_use_amt value for the failing currency","Default empty strings to \"0\" before parsing so the no-position skip path (returns Ok(None)) works","Sanitize the balance payload upstream before calling the parser","Upgrade the OKX adapter in case newer versions normalize this field"],"exampleFix":"// before\nlet spot_in_use_dec = Decimal::from_str(spot_in_use_str).map_err(|e| anyhow!(\"Failed to parse spotInUseAmt '{spot_in_use_str}': {e}\"))?;\n// after\nlet spot_in_use_str = spot_in_use_str.trim();\nlet spot_in_use_dec = if spot_in_use_str.is_empty() { Decimal::ZERO } else { Decimal::from_str(spot_in_use_str).map_err(|e| anyhow!(\"Failed to parse spotInUseAmt '{spot_in_use_str}': {e}\"))? };","handlingStrategy":"validation","validationCode":"let siu = balance.spot_in_use_amt.as_deref().unwrap_or(\"0\").trim();\nif siu.is_empty() || rust_decimal::Decimal::from_str(siu).is_err() { /* default to \"0\" or skip record */ }","typeGuard":"fn valid_spot_in_use(s: &str) -> bool {\n    let t = s.trim(); !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(\"spotInUseAmt\") => log::warn!(\"bad spot_in_use_amt '{}': {e}\", balance.spot_in_use_amt),\n    Err(e) => return Err(e),\n}","preventionTips":["Trim and default empty spot_in_use_amt to \"0\" before parsing","Re-validate balance parsing after OKX API upgrades","Keep fixtures covering both empty-string and populated fields","Fail soft (skip record) for non-critical balance entries"],"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-14T00:17:10.932Z"}