nautechsystems/nautilus_trader · error · anyhow::Error

Cannot cache futures spread: expiration dates differ call_in

Error message

Cannot cache futures spread: expiration dates differ call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}

What it means

cache_futures_spread requires both legs to expire at the same time, since the implied future price is computed to a single expiry. If call_instrument.expiration_ns() differs from put_instrument.expiration_ns() the method bails with this error. This catches pairs drawn from different expiries (calendar-style pairs) which parity math cannot handle.

Source

Thrown at crates/common/src/greeks.rs:1111

            anyhow::bail!(
                "Cannot cache futures spread: missing put underlying for {put_instrument_id}"
            );
        };

        if call_underlying != put_underlying {
            anyhow::bail!(
                "Cannot cache futures spread: option underlyings differ call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}"
            );
        }

        if call_instrument.strike_price() != put_instrument.strike_price() {
            anyhow::bail!(
                "Cannot cache futures spread: strike prices differ call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}"
            );
        }

        if call_instrument.expiration_ns() != put_instrument.expiration_ns() {
            anyhow::bail!(
                "Cannot cache futures spread: expiration dates differ call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}"
            );
        }

        let reference_future_price = self.get_price_object(&futures_instrument_id).ok_or_else(|| {
            anyhow::anyhow!(
                "Cannot cache futures spread: no reference futures price for {futures_instrument_id}"
            )
        })?;
        let call_price = self.get_price(&call_instrument_id).ok_or_else(|| {
            anyhow::anyhow!(
                "Cannot cache futures spread: missing option price for {call_instrument_id}"
            )
        })?;
        let put_price = self.get_price(&put_instrument_id).ok_or_else(|| {
            anyhow::anyhow!(
                "Cannot cache futures spread: missing option price for {put_instrument_id}"
            )

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Only pair options with identical expiration_ns (compare the raw timestamps, not formatted dates).
  2. Normalize expiry parsing so both legs resolve to the same expiration_ns value.
  3. Filter the option chain by exact expiration before choosing the call/put pair.

Example fix

// before
let price = greeks.cache_futures_spread(may_call_id, jun_put_id, future_id)?;
// after
if call.expiration_ns() != put.expiration_ns() {
    tracing::warn!("expiry mismatch, skipping pair");
    return Ok(None);
}
let price = greeks.cache_futures_spread(may_call_id, may_put_id, future_id)?;
Defensive patterns

Strategy: validation

Validate before calling

// rust
fn same_expiry(cache: &Cache, call_id: &InstrumentId, put_id: &InstrumentId) -> bool {
    cache.instrument(call_id).and_then(|i| i.expiration_ns()) == cache.instrument(put_id).and_then(|i| i.expiration_ns())
}

Type guard

fn expiry_ns(cache: &Cache, id: &InstrumentId) -> Option<UnixNanos> {
    cache.instrument(id).and_then(|i| i.expiration_ns())
}

Try / catch

match greeks.cache_futures_spread(call_id, put_id, future_id) {
    Ok(p) => use(p),
    Err(e) if e.to_string().contains("expiration dates differ") => rebuild_chain_for_expiry(expiry),
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Passing a near-month call with a far-month put; expiry timestamps parsed from different timezones or sources yielding unequal expiration_ns for nominally the same expiry; weekly vs monthly options mixed up.

Common situations: Pair-selection code that matches on expiry date strings but the raw ns timestamps differ (e.g. 16:00 vs 09:30 settlement); vendor data with differing expiry conventions per leg.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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