{"record":{"id":"9c82adadd2fd41f4","repo":"nautechsystems/nautilus_trader","slug":"missing-fee","errorCode":null,"errorMessage":"missing fee","messagePattern":"missing fee","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"crates/adapters/okx/src/common/parse.rs","lineNumber":445,"sourceCode":"/// OKX represents *charges* as positive numbers but they reduce the account\n/// balance, hence the value is negated.\n///\n/// # Errors\n///\n/// Returns an error if the fee is missing or empty, cannot be parsed into\n/// `Decimal`, or fails internal validation in [`Money::from_decimal`].\npub fn parse_fee(value: Option<&str>, currency: Currency) -> anyhow::Result<Money> {\n    // OKX uses opposite sign convention: negative = cost, positive = rebate.\n    // Negate to match Nautilus convention: positive = cost, negative = rebate.\n    let decimal = required_fee_amount(value)?;\n    Money::from_decimal(-decimal, currency).map_err(Into::into)\n}\n\nfn required_fee_amount(value: Option<&str>) -> anyhow::Result<Decimal> {\n    let value = value\n        .map(str::trim)\n        .filter(|fee| !fee.is_empty())\n        .ok_or_else(|| anyhow::anyhow!(\"missing fee\"))?;\n    Decimal::from_str(value).map_err(Into::into)\n}\n\n/// Parses OKX fee currency code, handling empty strings.\n///\n/// OKX sometimes returns empty fee currency codes.\n/// When the fee currency is empty, defaults to USDT and logs a warning for non-zero fees.\npub fn parse_fee_currency(\n    fee_ccy: &str,\n    fee_amount: Decimal,\n    context: impl FnOnce() -> String,\n) -> Currency {\n    let trimmed = fee_ccy.trim();\n    if trimmed.is_empty() {\n        if !fee_amount.is_zero() {\n            let ctx = context();\n            log::warn!(\n                \"Empty fee_ccy in {ctx} with non-zero fee={fee_amount}, using USDT as fallback\"","sourceCodeStart":427,"sourceCodeEnd":463,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/adapters/okx/src/common/parse.rs#L427-L463","documentation":"required_fee_amount normalizes an optional fee string from an OKX fill message: it trims whitespace and rejects None or empty strings with the terse 'missing fee' error before parsing the value as Decimal. OKX legitimately omits or blanks the fee field on some fills, so this conversion is strict by design — a fill without a fee string cannot produce a Commission.","triggerScenarios":"parse_fee, parse_fill_report, or parse_spread_fill_report encounters a fill whose fee (feeCcy/fee string) is missing or empty after trimming, and the code path requires a fee amount to build the Commission for the fill report.","commonSituations":"Maker fills with zero/rebated fees reported as empty by OKX; spread fills where fee details aren't populated; API schema changes renaming fee fields so the parser reads a now-empty field; partial-liquidation fills omitting fee fields.","solutions":["If a missing fee should mean zero fee for your use case, treat None/empty as Decimal::ZERO before calling required_fee_amount (or pass Some(\"0\") after defaulting).","Check whether the fill is a spread fill or rebate case where OKX reports fee=0/empty, and handle it as zero with a warning instead of erroring.","Log the raw fill JSON when this fires to confirm which fee field is blank and whether the adapter is reading the right field name for the current OKX API version.","Update the adapter if OKX changed its fill-message schema (fee field renamed/moved)."],"exampleFix":"// before\nlet fee = required_fee_amount(fill.fee.as_deref())?;\n\n// after\nlet fee = match fill.fee.as_deref().map(str::trim) {\n    None | Some(\"\") => Decimal::ZERO, // OKX omits fee on rebate/zero-fee fills\n    Some(v) => required_fee_amount(Some(v))?,\n};","handlingStrategy":"fallback","validationCode":"fn fee_or_zero(fee: Option<&str>) -> Decimal {\n    fee.map(str::trim)\n        .filter(|f| !f.is_empty())\n        .and_then(|f| Decimal::from_str(f).ok())\n        .unwrap_or(Decimal::ZERO)\n}","typeGuard":null,"tryCatchPattern":"let fee = required_fee_amount(msg.fee.as_deref())\n    .unwrap_or_else(|_| { log::debug!(\"OKX fill had no fee field; assuming 0\"); Decimal::ZERO });","preventionTips":["Expect empty/absent fee fields on OKX rebate and spread fills and default them to zero deliberately.","Log raw fill payloads once when a missing fee is first seen to confirm the schema.","Pin the adapter version to the OKX API version in use and update both together."],"tags":["rust","okx","fees","parsing"],"backgroundTag":"empty-required-field","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"}