{"record":{"id":"90bda319503692f7","repo":"nautechsystems/nautilus_trader","slug":"failed-to-parse-avg-px","errorCode":null,"errorMessage":"Failed to parse avg_px='{}': {}","messagePattern":"Failed to parse avg_px='(.+?)': (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/adapters/okx/src/websocket/parse.rs","lineNumber":1803,"sourceCode":"    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\n            };\n\n        // Convert quote quantity to base: quantity_base = sz_quote / price\n        let quantity_base = if let Some(price) = conversion_price_dec {\n            if price.is_zero() {\n                parse_quantity(&msg.sz, size_precision)?\n            } else {\n                Quantity::from_decimal_dp(sz_quote_dec / price, size_precision)?\n            }\n        } else {\n            // No price available, can't convert - use sz as-is temporarily\n            // This will be corrected once the order gets filled and price is available\n            parse_quantity(&msg.sz, size_precision)?\n        };\n","sourceCodeStart":1785,"sourceCodeEnd":1821,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/adapters/okx/src/websocket/parse.rs#L1785-L1821","documentation":"For quote-quantity market orders without a usable limit price, the parser uses the average fill price msg.avg_px as the conversion price (when it is non-empty and not \"0\"). If avg_px is present but fails Decimal::from_str, this error is raised. It means the reported average fill price on the order update is malformed or non-numeric.","triggerScenarios":"A quote-quantity market order update (SPOT BUY with tgtCcy=quote_ccy or the tgt_ccy-absent heuristic) where px is the market sentinel and avg_px is a non-empty, non-\"0\" string that fails Decimal::from_str — e.g. whitespace, corrupt value from a malformed/replayed message.","commonSituations":"Replaying recorded WebSocket sessions with corrupt avg_px fields; hand-built test OKXOrderMsg values with invalid avg_px; an OKX API format change for avgPx on certain order categories.","solutions":["Log the raw msg.avg_px to see the offending value.","Treat unparseable avg_px as absent (None) so conversion falls back to using sz as-is, matching the no-price branch.","Trim/normalize the string before Decimal::from_str.","Fix corrupt fixtures or check for an OKX payload format change."],"exampleFix":"// before (parse.rs:1802)\nSome(Decimal::from_str(&msg.avg_px).map_err(|e| {\n    anyhow::anyhow!(\"Failed to parse avg_px='{}': {}\", msg.avg_px, e)\n})?)\n\n// after\nmatch Decimal::from_str(msg.avg_px.trim()) {\n    Ok(avg) => Some(avg),\n    Err(e) => {\n        tracing::warn!(avg_px = %msg.avg_px, \"Unparseable avg_px, skipping conversion: {e}\");\n        None // fall back to using sz as-is until a fill price is available\n    }\n}","handlingStrategy":"validation","validationCode":"// Rust: validate avg_px is a usable conversion price before relying on it\nfn usable_avg_px(avg_px: &str) -> Option<Decimal> {\n    if avg_px.is_empty() || avg_px == \"0\" {\n        return None;\n    }\n    Decimal::from_str(avg_px.trim()).ok()\n}","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 avg_px=\") => {\n        tracing::warn!(ord_id = %msg.ord_id, avg_px = %msg.avg_px, \"malformed avg_px on order update\");\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Fall back gracefully: an unparseable avg_px should behave like \"0\" (no conversion price), not abort parsing.","Validate avg_px in any synthetic OKXOrderMsg used in tests or replay tooling.","Re-verify recorded fixtures after OKX API upgrades for numeric field format changes.","Centralize numeric-string parsing in one defensive helper with trim + error logging."],"tags":["rust","okx","decimal-parsing","avg-fill-price","quote-quantity"],"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"}