{"record":{"id":"9dc2d2d0597d3d04","repo":"nautechsystems/nautilus_trader","slug":"cannot-cache-futures-spread-strike-prices-differ","errorCode":null,"errorMessage":"Cannot cache futures spread: strike prices differ call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}","messagePattern":"Cannot cache futures spread: strike prices differ call_instrument_id=(.+?) put_instrument_id=(.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/common/src/greeks.rs","lineNumber":1105,"sourceCode":"        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!(\n                \"Cannot cache futures spread: no reference futures price for {futures_instrument_id}\"\n            )\n        })?;\n        let call_price = self.get_price(&call_instrument_id).ok_or_else(|| {\n            anyhow::anyhow!(\n                \"Cannot cache futures spread: missing option price for {call_instrument_id}\"","sourceCodeStart":1087,"sourceCodeEnd":1123,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/common/src/greeks.rs#L1087-L1123","documentation":"cache_futures_spread enforces put-call parity, so both legs must share the same strike. When call_instrument.strike_price() differs from put_instrument.strike_price() the method bails, since combining different strikes would not yield a valid implied future price.","triggerScenarios":"Passing a call and put with different strikes (e.g. from a strangle rather than an at-the-money parity pair); mis-parsing strike from symbol strings (ES 6000 call vs ES 6050 put); rounding differences in strike parsing.","commonSituations":"Symbol-to-ID parsing bugs where the strike field is truncated or misread; pairing logic that matches by expiry only; feeds that quote strikes in different tick sizes.","solutions":["Select the pair at the same strike (typically at-the-money) before calling cache_futures_spread.","Compare strike_price() on both instruments before invoking and skip unequal pairs.","Fix the strike parser if strikes were extracted incorrectly from instrument symbols."],"exampleFix":"// before\nlet price = greeks.cache_futures_spread(call_6000_id, put_6050_id, future_id)?;\n// after\nif call.strike_price() != put.strike_price() {\n    tracing::warn!(\"strike mismatch: {} vs {}\", call.strike_price().unwrap(), put.strike_price().unwrap());\n    return Ok(None);\n}\nlet price = greeks.cache_futures_spread(call_6000_id, put_6000_id, future_id)?;","handlingStrategy":"validation","validationCode":"// rust\nfn same_strike(cache: &Cache, call_id: &InstrumentId, put_id: &InstrumentId) -> bool {\n    cache.instrument(call_id).map(|i| i.strike_price()) == cache.instrument(put_id).map(|i| i.strike_price())\n}","typeGuard":"fn strike_of(cache: &Cache, id: &InstrumentId) -> Option<Price> {\n    cache.instrument(id).and_then(|i| i.strike_price())\n}","tryCatchPattern":"let res = greeks.cache_futures_spread(call_id, put_id, future_id);\nif let Err(e) = res {\n    if e.to_string().contains(\"strike prices differ\") {\n        reselect_pair_at_same_strike();\n    }\n}","preventionTips":["Select parity pairs by (underlying, expiry, strike) composite key.","Test strike parsing against vendor symbol formats to avoid truncation.","Reject strangles/strangles-like combinations early — they are not parity pairs."],"tags":["rust","validation","strike-price","options"],"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"}