{"record":{"id":"1da95f345b214e03","repo":"nautechsystems/nautilus_trader","slug":"cannot-cache-futures-spread-missing-put-underlyin","errorCode":null,"errorMessage":"Cannot cache futures spread: missing put underlying for {put_instrument_id}","messagePattern":"Cannot cache futures spread: missing put underlying for (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/common/src/greeks.rs","lineNumber":1093,"sourceCode":"                \"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}\"\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!(","sourceCodeStart":1075,"sourceCodeEnd":1111,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/common/src/greeks.rs#L1075-L1111","documentation":"Mirror of the call-side check: cache_futures_spread reads the put option's underlying symbol, and if underlying() is None for the put instrument it bails with this error naming the put instrument. Without the put's underlying the method cannot build the underlying futures ID used as the spread cache key.","triggerScenarios":"Calling cache_futures_spread with a put instrument whose definition lacks an underlying — hand-built fixtures or adapters that leave underlying unset for the put leg.","commonSituations":"Test fixtures that populate the call fully but build the put minimally; adapters that only set underlying on one leg; serialized instrument definitions with the field dropped.","solutions":["Set the underlying field on the put instrument definition before adding it to the Cache.","Fix the adapter/loader so underlying is populated for all option instruments.","Pre-check put.underlying().is_some() at the call site and skip with a warning if absent."],"exampleFix":"// before\nInstrument::new(... /* put built without underlying */ ...)\n// after\nInstrument::new(..., Some(underlying_symbol), ...) // put leg must carry the same underlying as the call","handlingStrategy":"validation","validationCode":"// rust\nfn put_has_underlying(cache: &Cache, put_id: &InstrumentId) -> bool {\n    cache.instrument(put_id).and_then(|i| i.underlying()).is_some()\n}\nif !put_has_underlying(&cache, &put_id) {\n    tracing::warn!(\"put {put_id} has no underlying; skipping spread\");\n    return Ok(None);\n}","typeGuard":"fn put_underlying(cache: &Cache, id: &InstrumentId) -> Option<Ustr> {\n    cache.instrument(id).filter(|i| i.option_kind() == Some(OptionKind::Put)).and_then(|i| i.underlying())\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(\"missing put underlying\") {\n        reload_instrument_definition(put_id);\n    }\n}","preventionTips":["Apply the same definition validation to every option leg, not just the first.","Check fixture builders in tests so put instruments are constructed identically to calls.","Log and quarantine malformed instrument definitions at ingestion."],"tags":["rust","instrument","underlying","options"],"backgroundTag":"missing-required-argument","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"}