{"record":{"id":"e1998b068c45a063","repo":"nautechsystems/nautilus_trader","slug":"ax-instrument-product-must-be-non-empty-without-su","errorCode":null,"errorMessage":"AX instrument product must be non-empty without surrounding whitespace, was '{product}'","messagePattern":"AX instrument product must be non-empty without surrounding whitespace, was '(.+?)'","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/adapters/architect_ax/src/http/parse.rs","lineNumber":179,"sourceCode":"    definition: &AxInstrument,\n    maker_fee: Decimal,\n    taker_fee: Decimal,\n    ts_event: UnixNanos,\n    ts_init: UnixNanos,\n) -> anyhow::Result<InstrumentAny> {\n    let raw_symbol_str = definition.symbol.as_str();\n    let raw_symbol = Symbol::new(raw_symbol_str);\n    let instrument_id = InstrumentId::new(raw_symbol, *AX_VENUE);\n\n    let symbol_prefix = raw_symbol_str\n        .split('-')\n        .next()\n        .context(\"Failed to extract symbol prefix\")?;\n\n    let underlying = match definition.product {\n        Some(product) => {\n            let trimmed = product.trim();\n            anyhow::ensure!(\n                !trimmed.is_empty() && trimmed == product,\n                \"AX instrument product must be non-empty without surrounding whitespace, was '{product}'\"\n            );\n            product\n        }\n        None => Ustr::from(symbol_prefix),\n    };\n\n    // Derive base code by stripping quote currency suffix if present\n    // e.g. JPYUSD-PERP → base=JPY, BTC-PERP → base=BTC\n    let quote_code = definition.quote_currency.as_str();\n    let base_code = if symbol_prefix.ends_with(quote_code) && symbol_prefix.len() > quote_code.len()\n    {\n        &symbol_prefix[..symbol_prefix.len() - quote_code.len()]\n    } else {\n        symbol_prefix\n    };\n","sourceCodeStart":161,"sourceCodeEnd":197,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/adapters/architect_ax/src/http/parse.rs#L161-L197","documentation":"The AX instrument definition's `product` field, when present, must be a non-empty string with no leading or trailing whitespace. The adapter trims the value and rejects it if trimming changes it or if it is empty, because the product underlies symbol parsing and instrument ID construction.","triggerScenarios":"Calling request_instruments or request_instrument, which invoke parse_instrument, on an AX instrument definition whose `product` is an empty string, whitespace-only, or contains surrounding whitespace (e.g. \" BTC\" or \"BTC \").","commonSituations":"Upstream AX API schema change or data-entry error in the exchange's instrument metadata, hand-edited mock/fixture JSON with a malformed product field, or a stale adapter parsing a newer API payload format.","solutions":["Inspect the raw /instrument payload for the offending instrument and confirm the product value.","Trim the product value at the source (venue feed or fixture) before it reaches the adapter.","If the API now returns padded values, report/work around by normalizing the payload upstream (e.g. in a proxy or by updating the adapter's deserialization).","Update stale fixtures/mocks to carry clean product values."],"exampleFix":"// before (fixture)\n{ \"symbol\": \"BTC-USDT\", \"product\": \" BTC \" }\n// after\n{ \"symbol\": \"BTC-USDT\", \"product\": \"BTC\" }","handlingStrategy":"validation","validationCode":"fn check_product(product: Option<&str>) -> bool {\n    match product {\n        None => true,\n        Some(p) => !p.trim().is_empty() && p.trim() == p,\n    }\n}","typeGuard":"fn has_clean_product(d: &InstrumentDefinition) -> bool {\n    d.product.as_ref().map_or(true, |p| !p.trim().is_empty() && p.trim() == p.as_str())\n}","tryCatchPattern":null,"preventionTips":["Sanitize venue payloads (trim strings) in a normalization layer before parsing","Validate fixtures against the same invariants as production data","Watch for AX API schema changes affecting the product field"],"tags":["rust","validation","schema","instrument"],"backgroundTag":"schema-validation-failed","analyzedSha":"18893faf8b356be3320add8de2f861b0b647cf06","analyzedAt":"2026-09-08T20:49:34.690Z","contentChangedAt":"2026-09-08T20:49:34.690Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}