{"record":{"id":"90166ea0be783f21","repo":"nautechsystems/nautilus_trader","slug":"cannot-cache-futures-spread-expected-call-put-pai","errorCode":null,"errorMessage":"Cannot cache futures spread: expected call/put pair call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}","messagePattern":"Cannot cache futures spread: expected call/put pair call_instrument_id=(.+?) put_instrument_id=(.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/common/src/greeks.rs","lineNumber":1082,"sourceCode":"        };\n        let Some(reference_future_instrument) = reference_future_instrument else {\n            anyhow::bail!(\n                \"Cannot cache futures spread: no reference futures instrument for {futures_instrument_id}\"\n            );\n        };\n\n        if call_instrument.instrument_class() != InstrumentClass::Option\n            || put_instrument.instrument_class() != InstrumentClass::Option\n        {\n            anyhow::bail!(\n                \"Cannot cache futures spread: non-option instruments provided call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}\"\n            );\n        }\n\n        if call_instrument.option_kind() != Some(OptionKind::Call)\n            || put_instrument.option_kind() != Some(OptionKind::Put)\n        {\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}\"","sourceCodeStart":1064,"sourceCodeEnd":1100,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/common/src/greeks.rs#L1064-L1100","documentation":"After confirming both instruments are options, cache_futures_spread checks option_kind(): the first must be OptionKind::Call and the second OptionKind::Put. If either is the wrong kind (put passed as call, two calls, two puts) the method bails, because the put-call parity spread computation requires exactly one call and one put.","triggerScenarios":"Passing two calls or two puts as the pair; swapping the call and put arguments so option_kind checks fail in both directions; instruments whose option_kind metadata is missing or set incorrectly by the adapter.","commonSituations":"Constructing the pair from a listing where both legs share a strike/expiry but the code grabs the wrong option_kind; refactored code that dropped an ordering guarantee; adapter loading option_kind as None.","solutions":["Check option_kind() on both instruments before calling: first must be Some(OptionKind::Call), second Some(OptionKind::Put).","If you only have an unordered pair, sort the legs by option_kind and pass call first, put second.","Verify the adapter populated option_kind correctly for both cached instruments."],"exampleFix":"// before\nlet price = greeks.cache_futures_spread(leg_a_id, leg_b_id, future_id)?;\n// after\nlet (call_id, put_id) = if cache.instrument(&leg_a_id).unwrap().option_kind() == Some(OptionKind::Call) {\n    (leg_a_id, leg_b_id)\n} else {\n    (leg_b_id, leg_a_id)\n};\nlet price = greeks.cache_futures_spread(call_id, put_id, future_id)?;","handlingStrategy":"validation","validationCode":"// rust\nfn ordered_pair(cache: &Cache, a: &InstrumentId, b: &InstrumentId) -> Option<(InstrumentId, InstrumentId)> {\n    let ka = cache.instrument(a)?.option_kind()?;\n    let kb = cache.instrument(b)?.option_kind()?;\n    match (ka, kb) {\n        (OptionKind::Call, OptionKind::Put) => Some((a.clone(), b.clone())),\n        (OptionKind::Put, OptionKind::Call) => Some((b.clone(), a.clone())),\n        _ => None,\n    }\n}","typeGuard":"fn call_leg(cache: &Cache, id: &InstrumentId) -> Option<&InstrumentAny> {\n    cache.instrument(id).filter(|i| i.option_kind() == Some(OptionKind::Call))\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(\"expected call/put pair\") => reorder_legs_and_retry(),\n    Err(e) => return Err(e),\n}","preventionTips":["Store option legs in structs that name fields explicitly (call_id, put_id) instead of ordered tuples.","Normalize pair ordering at ingestion time using option_kind.","Validate adapter-provided option_kind is Some for all loaded options."],"tags":["rust","validation","option-kind","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-14T00:17:10.932Z"}