{"record":{"id":"4c50c5bdaf2658ef","repo":"nautechsystems/nautilus_trader","slug":"cannot-cache-futures-spread-option-underlyings-di","errorCode":null,"errorMessage":"Cannot cache futures spread: option underlyings differ call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}","messagePattern":"Cannot cache futures spread: option underlyings differ call_instrument_id=(.+?) put_instrument_id=(.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/common/src/greeks.rs","lineNumber":1099,"sourceCode":"        {\n            anyhow::bail!(\n                \"Cannot cache futures spread: expected call/put pair call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}\"\n            );\n        }\n\n        let Some(call_underlying) = call_instrument.underlying() else {\n            anyhow::bail!(\n                \"Cannot cache futures spread: missing call underlying for {call_instrument_id}\"\n            );\n        };\n        let Some(put_underlying) = put_instrument.underlying() else {\n            anyhow::bail!(\n                \"Cannot cache futures spread: missing put underlying for {put_instrument_id}\"\n            );\n        };\n\n        if call_underlying != put_underlying {\n            anyhow::bail!(\n                \"Cannot cache futures spread: option underlyings differ call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}\"\n            );\n        }\n\n        if call_instrument.strike_price() != put_instrument.strike_price() {\n            anyhow::bail!(\n                \"Cannot cache futures spread: strike prices differ call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}\"\n            );\n        }\n\n        if call_instrument.expiration_ns() != put_instrument.expiration_ns() {\n            anyhow::bail!(\n                \"Cannot cache futures spread: expiration dates differ call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}\"\n            );\n        }\n\n        let reference_future_price = self.get_price_object(&futures_instrument_id).ok_or_else(|| {\n            anyhow::anyhow!(","sourceCodeStart":1081,"sourceCodeEnd":1117,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/common/src/greeks.rs#L1081-L1117","documentation":"cache_futures_spread requires the call and put legs to be a matched pair on the same underlying: the parity-based implied future price is only meaningful when both options reference the same underlying. If call_underlying != put_underlying the method bails with this error listing both instruments.","triggerScenarios":"Passing options from different underlyings (e.g. ESM6 options vs ESZ6 options) as the call/put pair; a copy-paste mistake pairing options across contract months or across symbols entirely.","commonSituations":"Loop code that pairs consecutive option listings without grouping by underlying; building calendars/diagonal-style structures and feeding them to a routine that expects same-strike/same-expiry parity pairs.","solutions":["Group option instruments by underlying and only pass call/put pairs sharing the same underlying.","Compare underlying() on both legs before calling and skip/log mismatched pairs.","Correct the ID source if the pairing was built from a mis-sorted or mis-keyed collection."],"exampleFix":"// before\nlet price = greeks.cache_futures_spread(esm6_call_id, esz6_put_id, esm6_future_id)?;\n// after\nif call.underlying() != put.underlying() {\n    tracing::warn!(\"skipping mismatched pair {call_id} / {put_id}\");\n    return Ok(None);\n}\nlet price = greeks.cache_futures_spread(esm6_call_id, esm6_put_id, esm6_future_id)?;","handlingStrategy":"validation","validationCode":"// rust\nfn same_underlying(cache: &Cache, call_id: &InstrumentId, put_id: &InstrumentId) -> bool {\n    cache.instrument(call_id).and_then(|i| i.underlying()) == cache.instrument(put_id).and_then(|i| i.underlying())\n}","typeGuard":"fn pair_key(cache: &Cache, call_id: &InstrumentId, put_id: &InstrumentId) -> Option<Ustr> {\n    let u = cache.instrument(call_id)?.underlying()?;\n    (cache.instrument(put_id)?.underlying()? == u).then_some(u)\n}","tryCatchPattern":"match greeks.cache_futures_spread(call_id, put_id, future_id) {\n    Ok(p) => use(p),\n    Err(e) if e.to_string().contains(\"underlyings differ\") => {\n        tracing::warn!(\"dropping cross-underlying pair {call_id}/{put_id}\");\n        Ok(None)\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Group options by underlying before forming call/put pairs.","Never pair options across contract months or symbols for parity computations.","Include underlying in the pairing key when indexing option chains."],"tags":["rust","validation","options","pairing"],"backgroundTag":"invalid-argument-value","analyzedSha":"18893faf8b356be3320add8de2f861b0b647cf06","analyzedAt":"2026-09-08T20:49:34.690Z","contentChangedAt":"2026-09-08T20:49:34.690Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}