nautechsystems/nautilus_trader · error · anyhow::Error

leg_contract_details must be provided

Error message

leg_contract_details must be provided

What it means

parse_spread_instrument_id builds an OptionSpread from IBKR contract details for each leg. It requires a non-empty leg_contract_details slice because it reads the first leg's contract details to derive the spread's metadata. An empty slice is a caller programming error, so it bails immediately before any parsing.

Source

Thrown at crates/adapters/interactive_brokers/src/providers/parse.rs:674

    InstrumentAny::from(instrument)
}

/// Parse a spread instrument ID into an OptionSpread instrument.
///
/// This implements the same logic as Python's `parse_spread_instrument_id`.
/// Uses contract details from the first leg to determine spread properties.
///
/// # Errors
///
/// Returns an error if parsing fails.
pub fn parse_spread_instrument_id(
    instrument_id: InstrumentId,
    leg_contract_details: &[(&ibapi::contracts::ContractDetails, i32)],
    timestamp_ns: Option<UnixNanos>,
) -> anyhow::Result<OptionSpread> {
    if leg_contract_details.is_empty() {
        anyhow::bail!("leg_contract_details must be provided");
    }

    // Use contract details from first leg
    let (first_details, _) = leg_contract_details[0];
    let first_contract = &first_details.contract;

    // Extract properties from the first leg contract details
    let currency = Currency::from(first_contract.currency.to_string());
    let underlying = if !first_details.under_symbol.is_empty() {
        Ustr::from(first_details.under_symbol.as_str())
    } else {
        Ustr::from(first_contract.symbol.as_str())
    };

    // Parse multiplier
    let multiplier_str = first_contract.multiplier.to_string();
    let multiplier =
        Quantity::from_str(&multiplier_str).unwrap_or_else(|_| Quantity::new(100.0, 0)); // Default to 100 for options

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Ensure each spread leg's ContractDetails is fetched successfully from the IBKR client before calling the parser; retry or log failed leg requests.
  2. Validate leg_contract_details is non-empty at the call site and surface a domain error identifying the missing instrument instead of reaching the parser.
  3. If parse_option_spread_instrument_id produced empty legs, verify the instrument_id parses to a valid option spread symbol and that the contract-details lookups use the correct exchange/currency.

Example fix

// before
let legs: Vec<(&ContractDetails, i32)> = Vec::new();
let spread = parse_spread_instrument_id(instrument_id, &legs, ts)?;
// after
if legs.is_empty() {
    anyhow::bail!("no contract details resolved for {instrument_id}");
}
let spread = parse_spread_instrument_id(instrument_id, &legs, ts)?;
Defensive patterns

Strategy: validation

Validate before calling

if leg_contract_details.is_empty() {
    return Err(anyhow::anyhow!("cannot parse spread {instrument_id}: no leg contract details"));
}

Prevention

When it happens

Trigger: Calling parse_spread_instrument_id (directly or via parse_option_spread_instrument_id) with an empty slice of (&ContractDetails, i32) leg tuples — e.g. when the IBKR contract-details request returned nothing for all legs or the caller built the leg list without pushing any entries.

Common situations: Fetching option chain contract details returned zero matches (wrong expiry/strike filters), a bug in collecting leg details before calling the parser, or passing a default-initialized/filtered-out leg list.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


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