nautechsystems/nautilus_trader · error · anyhow::Error

Failed to parse fee '{fee_str}': {e}

Error message

Failed to parse fee '{fee_str}': {e}

What it means

After confirming the fee string is present, the adapter converts it to a Decimal before computing the fee Money object. This error is thrown when Decimal::from_str fails on the fee string — the value OKX sent is not a valid decimal number.

Source

Thrown at crates/adapters/okx/src/websocket/parse.rs:2153

        } else {
            anyhow::bail!(
                "Cannot determine fill quantity: fill_sz is empty/zero and acc_fill_sz is empty/zero"
            );
        }
    } else {
        anyhow::bail!(
            "Cannot determine fill quantity: fill_sz='{}' and acc_fill_sz is None",
            msg.fill_sz
        );
    };

    let fee_str = msg
        .fee
        .as_deref()
        .filter(|fee| !fee.trim().is_empty())
        .ok_or_else(|| anyhow::anyhow!("missing fee for fill report inst_id={}", msg.inst_id))?;
    let fee_dec = Decimal::from_str(fee_str)
        .map_err(|e| anyhow::anyhow!("Failed to parse fee '{fee_str}': {e}"))?;

    let fee_currency = parse_fee_currency(msg.fee_ccy.as_str(), fee_dec, || {
        format!("fill report for inst_id={}", msg.inst_id)
    });

    // OKX sends fees as negative numbers (e.g., "-2.5" for a $2.5 charge), parse_fee negates to positive
    let total_fee = parse_fee(Some(fee_str), fee_currency)
        .map_err(|e| anyhow::anyhow!("Failed to parse fee={:?}: {}", msg.fee, e))?;

    // OKX sends cumulative fees, so we subtract the previous total to get this fill's fee
    let commission = if let Some(previous_fee) = previous_fee {
        if total_fee.currency == previous_fee.currency {
            let incremental = total_fee - previous_fee;

            if incremental < Money::zero(fee_currency) {
                log::debug!(
                    "Negative incremental fee detected - likely a maker rebate or fee refund: order_id={}, total_fee={}, previous_fee={}, incremental={}",
                    msg.ord_id.as_str(),

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Log msg.fee for the failing fill and check what non-decimal text was received.
  2. Verify no middleware or JSON layer is altering the raw WebSocket payload.
  3. If OKX changed the fee format (e.g. scientific notation), extend the adapter's parsing to normalize the string before Decimal::from_str.
  4. Resync fills via REST to confirm the expected format.

Example fix

// before
let fee_dec = Decimal::from_str(fee_str).map_err(...)?;
// after: normalize scientific notation first
let normalized = if fee_str.contains('e') || fee_str.contains('E') {
    rust_decimal::Decimal::from_scientific(fee_str)?.to_string()
} else { fee_str.to_string() };
let fee_dec = Decimal::from_str(&normalized).map_err(...)?;
Defensive patterns

Strategy: validation

Validate before calling

// Rust: check fee is a plain decimal before the adapter parses it
fn fee_is_decimal(fee: &str) -> bool {
    !fee.is_empty() && !fee.contains(['e', 'E', ','])
        && rust_decimal::Decimal::from_str(fee).is_ok()
}

Try / catch

if let Err(e) = parse_fill_report(&msg, ...) {
    log::warn!("fee parse failed (fee={:?}): {e}", msg.fee);
    fetch_fills_via_rest(&msg.inst_id); // fees available authoritatively there
}

Prevention

When it happens

Trigger: parse_fill_report receives a fee string that Decimal::from_str cannot parse: non-numeric text, scientific notation if unsupported, thousands separators, or locale-formatted values.

Common situations: OKX API format changes; corrupted or hand-modified test payloads; proxy/middleware mangling numeric fields in WebSocket JSON.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/e0f9bb99f2831c14. Report an issue: GitHub.