{"record":{"id":"c624be20ebe82a86","repo":"nautechsystems/nautilus_trader","slug":"failed-to-parse-sz-as-quote-quantity","errorCode":null,"errorMessage":"Failed to parse sz='{}' as quote quantity: {}","messagePattern":"Failed to parse sz='(.+?)' as quote quantity: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/adapters/okx/src/websocket/parse.rs","lineNumber":1788,"sourceCode":"    // OKX always returns acc_fill_sz in base currency, but sz depends on tgt_ccy\n\n    // Determine if this is a quote-quantity order\n    // Method 1: Explicit tgt_ccy field set to QuoteCcy\n    let is_quote_qty_explicit = msg.tgt_ccy == Some(OKXTargetCurrency::QuoteCcy);\n\n    // Method 2: Use OKX defaults when tgt_ccy is None (old orders or missing field)\n    // OKX API defaults for SPOT market orders: BUY orders use quote_ccy, SELL orders use base_ccy\n    // Note: tgtCcy only applies to SPOT market orders (not limit orders)\n    // For limit orders, sz is always in base currency regardless of side\n    let is_quote_qty_heuristic = msg.tgt_ccy.is_none()\n        && (msg.inst_type == OKXInstrumentType::Spot || msg.inst_type == OKXInstrumentType::Margin)\n        && msg.side == OKXSide::Buy\n        && order_type == OrderType::Market;\n\n    let (quantity, filled_qty) = if is_quote_qty_explicit || is_quote_qty_heuristic {\n        // Quote-quantity order: sz is in quote currency, need to convert to base\n        let sz_quote_dec = Decimal::from_str(&msg.sz).map_err(|e| {\n            anyhow::anyhow!(\"Failed to parse sz='{}' as quote quantity: {}\", msg.sz, e)\n        })?;\n\n        // Determine the price to use for conversion\n        // Priority: 1) limit price (px) for limit orders, 2) avg_px for market orders\n        let conversion_price_dec =\n            if !is_market_price(&msg.px) {\n                // Limit order: use the limit price (msg.px)\n                Some(\n                    Decimal::from_str(&msg.px)\n                        .map_err(|e| anyhow::anyhow!(\"Failed to parse px='{}': {}\", msg.px, e))?,\n                )\n            } else if !msg.avg_px.is_empty() && msg.avg_px != \"0\" {\n                // Market order with fills: use average fill price\n                Some(Decimal::from_str(&msg.avg_px).map_err(|e| {\n                    anyhow::anyhow!(\"Failed to parse avg_px='{}': {}\", msg.avg_px, e)\n                })?)\n            } else {\n                None","sourceCodeStart":1770,"sourceCodeEnd":1806,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/adapters/okx/src/websocket/parse.rs#L1770-L1806","documentation":"For quote-quantity orders (tgt_ccy=QuoteCcy, or the SPOT/BUY market-order heuristic), the parser treats msg.sz as a quote-currency amount and must parse it as a Decimal before dividing by a conversion price. If the sz string is not a valid decimal number, this error is raised. It indicates a malformed or non-numeric sz field on the OKX order message.","triggerScenarios":"A quote-quantity order status update (OKX SPOT market BUY with tgtCcy=quote_ccy, or tgtCcy absent on a SPOT/margin BUY market order) where msg.sz is not parseable by Decimal::from_str — e.g. empty string, scientific notation with unexpected format, thousands separators, or placeholder values from a malformed message.","commonSituations":"Replaying malformed/hand-edited test fixtures or recorded WebSocket logs with corrupt sz; an OKX API change emitting sz in an unexpected numeric format; deserialization defaulting sz to \"\" when the field is missing on older messages.","solutions":["Log the raw msg.sz and surrounding message JSON to see the actual value received.","Guard before parsing: skip conversion (treat as base quantity via parse_quantity) when sz is empty or non-numeric.","Normalize the string (trim whitespace, strip separators) before Decimal::from_str.","Check for an OKX API format change and update the deserialization/model accordingly."],"exampleFix":"// before (parse.rs:1787)\nlet sz_quote_dec = Decimal::from_str(&msg.sz).map_err(|e| {\n    anyhow::anyhow!(\"Failed to parse sz='{}' as quote quantity: {}\", msg.sz, e)\n})?;\n\n// after\nlet sz_quote_dec = if msg.sz.is_empty() {\n    return Ok(/* fall back to base-quantity parsing or zero quantity */);\n} else {\n    Decimal::from_str(msg.sz.trim()).map_err(|e| {\n        anyhow::anyhow!(\"Failed to parse sz='{}' as quote quantity: {}\", msg.sz, e)\n    })?\n};","handlingStrategy":"validation","validationCode":"// Rust: validate sz is a non-empty decimal string before processing\nfn valid_decimal_str(s: &str) -> bool {\n    !s.trim().is_empty() && Decimal::from_str(s.trim()).is_ok()\n}\n// gate: if quote-quantity path chosen, require valid_decimal_str(&msg.sz)","typeGuard":"fn parseable_decimal(s: &str) -> Option<Decimal> {\n    Decimal::from_str(s.trim()).ok()\n}","tryCatchPattern":"match parse_order_status_report(&msg, &instrument, account_id, ts_init) {\n    Ok(report) => handle(report),\n    Err(e) if e.to_string().contains(\"Failed to parse sz\") => {\n        tracing::warn!(ord_id = %msg.ord_id, sz = %msg.sz, \"malformed sz on quote-quantity order\");\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Never hand-edit recorded WebSocket fixtures; regenerate them from live captures.","Validate Decimal-parseability of sz/px/avg_px when constructing OKXOrderMsg in tests.","Trim and normalize numeric strings at the deserialization layer.","Monitor for OKX API changes to the sz field format for spot market orders."],"tags":["rust","okx","decimal-parsing","quantity-conversion","spot-market-orders"],"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"}