nautechsystems/nautilus_trader · error

Missing underlying for {}

Error message

Missing underlying for {}

What it means

Same required-field guard family in the instrument message TryFrom: `underlying` is null in the received BitmexInstrumentMsg, so the full instrument definition cannot be constructed and the conversion fails.

Source

Thrown at crates/adapters/bitmex/src/websocket/messages.rs:585

    pub fair_basis: Option<f64>,
    pub fair_basis_rate: Option<f64>,
    pub fair_price: Option<f64>,
    pub timestamp: Timestamp,
}

impl TryFrom<BitmexInstrumentMsg> for crate::http::models::BitmexInstrument {
    type Error = anyhow::Error;

    fn try_from(msg: BitmexInstrumentMsg) -> Result<Self, Self::Error> {
        use crate::common::enums::{BitmexInstrumentState, BitmexInstrumentType};

        // Required fields
        let root_symbol = msg
            .root_symbol
            .ok_or_else(|| anyhow::anyhow!("Missing root_symbol for {}", msg.symbol))?;
        let underlying = msg
            .underlying
            .ok_or_else(|| anyhow::anyhow!("Missing underlying for {}", msg.symbol))?;
        let quote_currency = msg
            .quote_currency
            .ok_or_else(|| anyhow::anyhow!("Missing quote_currency for {}", msg.symbol))?;
        let tick_size = msg
            .tick_size
            .ok_or_else(|| anyhow::anyhow!("Missing tick_size for {}", msg.symbol))?;
        let multiplier = msg
            .multiplier
            .ok_or_else(|| anyhow::anyhow!("Missing multiplier for {}", msg.symbol))?;
        let is_quanto = msg
            .is_quanto
            .ok_or_else(|| anyhow::anyhow!("Missing is_quanto for {}", msg.symbol))?;
        let is_inverse = msg
            .is_inverse
            .ok_or_else(|| anyhow::anyhow!("Missing is_inverse for {}", msg.symbol))?;

        // Parse state - default to Open if not present
        let state = msg

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Catch and skip the record; process instruments that do carry underlying.
  2. Cache full snapshots and apply partial updates only to cached definitions.
  3. Filter subscriptions to standard derivative symbols with complete definitions.
  4. Update adapter field mappings if BitMEX changed the message schema.

Example fix

// before
let instrument = TryInto::<Instrument>::try_into(msg)?;
// after
if let Err(e) = TryInto::<Instrument>::try_into(msg) {
    log::debug!("Ignoring incomplete instrument update: {e}");
    return Ok(());
}
Defensive patterns

Strategy: fallback

Validate before calling

if msg.underlying.is_none() { /* defer to next full snapshot */ }

Type guard

fn has_definition(msg: &BitmexInstrumentMsg) -> bool {
    msg.root_symbol.is_some() && msg.underlying.is_some() && msg.quote_currency.is_some() && msg.tick_size.is_some()
}

Try / catch

let def = match TryInto::<InstrumentDef>::try_into(msg) {
    Ok(d) => d,
    Err(e) => { debug!("skipping: {e}"); return Ok(()); }
};

Prevention

When it happens

Trigger: A WS `instrument` message for a symbol (typically derivatives) arriving with underlying: null — partial snapshots, spot/index symbols, or delisted contracts.

Common situations: Bulk instrument subscriptions including non-derivative symbols; BitMEX emitting deltas where absent fields are null rather than omitted; older API messages missing recently added/required fields.

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


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