nautechsystems/nautilus_trader · error · anyhow::Error

Empty expiry string

Error message

Empty expiry string

What it means

expiry_timestring_to_unix_nanos converts an IB contract expiry string (e.g. a futures or option last-trade date) into a UnixNanos timestamp. IB expiry strings can legitimately be empty for some contracts, but a parseable expiry is required to build the instrument, so an empty string is rejected immediately.

Source

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

        parts[1].len().min(8) as u8
    } else {
        0
    }
}

/// Convert timestamp string to UnixNanos.
///
/// Handles formats like "20230101" or "20230101 00:00:00 UTC".
///
/// # Errors
///
/// Returns an error if the timestamp cannot be parsed.
pub fn expiry_timestring_to_unix_nanos(
    expiry: &str,
    details: Option<&ibapi::contracts::ContractDetails>,
) -> anyhow::Result<UnixNanos> {
    if expiry.is_empty() {
        anyhow::bail!("Empty expiry string");
    }

    // Parse timestamp string - Most contract expirations are %Y%m%d format
    // Some exchanges have expirations in %Y%m%d %H:%M:%S %Z
    let dt = if expiry.len() == 8 {
        // Format: YYYYMMDD
        let year = &expiry[0..4];
        let month = &expiry[4..6];
        let day = &expiry[6..8];
        let date = time::Date::from_calendar_date(
            year.parse()?,
            time::Month::try_from(month.parse::<u8>()?)?,
            day.parse()?,
        )?;

        // If we have trading hours, try to extract the last trade time
        // Trading hours format: "20240411:0000-20240411:1800;..."
        let mut expiry_time = time::Time::MIDNIGHT;

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Provide an expiry string (YYYYMMDD, optionally with time) in the contract details before parsing.
  2. For perpetual/index instruments that genuinely have no expiry, route to the appropriate parser (e.g. index/continuous future) rather than the futures/options parser.
  3. Check IB contract details are complete — re-request contractDetails if fields came back empty.
  4. Guard: skip contracts with empty expiry instead of failing the whole batch.

Example fix

// before: empty expiry
let details = ContractDetails { last_trade_date_or_contract_month: "".into(), .. };
// after: expiry supplied
let details = ContractDetails { last_trade_date_or_contract_month: "20260619".into(), .. };
Defensive patterns

Strategy: validation

Validate before calling

fn has_expiry(details: &ContractDetails) -> bool {
    !details.last_trade_date_or_contract_month.is_empty()
}

Prevention

When it happens

Trigger: parse_futures_contract or parse_option_contract calls expiry_timestring_to_unix_nanos with contract details whose last_trade_date_or_contract_month (or local expiry field) is empty.

Common situations: Continuous/index-style contracts with no discrete expiry, incomplete contract details returned from IB, or manually built ContractDetails omitting the expiry field.

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/e93ba4cd16825132. Report an issue: GitHub.