{"record":{"id":"2797a0e01a408f27","repo":"nautechsystems/nautilus_trader","slug":"cannot-cache-futures-spread-underlying-underlyin","errorCode":null,"errorMessage":"Cannot cache futures spread: underlying {underlying_instrument_id} is not a futures contract","messagePattern":"Cannot cache futures spread: underlying (.+?) is not a futures contract","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/common/src/greeks.rs","lineNumber":1141,"sourceCode":"                \"Cannot cache futures spread: missing option price for {call_instrument_id}\"\n            )\n        })?;\n        let put_price = self.get_price(&put_instrument_id).ok_or_else(|| {\n            anyhow::anyhow!(\n                \"Cannot cache futures spread: missing option price for {put_instrument_id}\"\n            )\n        })?;\n\n        let underlying_instrument_id =\n            InstrumentId::from(format!(\"{call_underlying}.{}\", call_instrument_id.venue));\n\n        // Reject if the underlying is present in cache but is not a future\n        {\n            let cache = self.cache.borrow();\n            if let Some(underlying) = cache.instrument(&underlying_instrument_id)\n                && underlying.instrument_class() != InstrumentClass::Future\n            {\n                anyhow::bail!(\n                    \"Cannot cache futures spread: underlying {underlying_instrument_id} is not a futures contract\"\n                );\n            }\n        }\n\n        let implied_future_price =\n            self.calculate_implied_future_price(&call_instrument, call_price, put_price);\n        let spread = implied_future_price - reference_future_price.as_f64();\n        let spread_price = reference_future_instrument.make_price(spread);\n\n        self.cached_futures_spreads.borrow_mut().insert(\n            underlying_instrument_id,\n            (futures_instrument_id, spread_price),\n        );\n\n        Ok(reference_future_price + spread_price)\n    }\n","sourceCodeStart":1123,"sourceCodeEnd":1159,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/common/src/greeks.rs#L1123-L1159","documentation":"After computing the underlying instrument ID from the options' underlying symbol and venue, cache_futures_spread checks the Cache: if an instrument exists under that ID but its class is not InstrumentClass::Future, the method bails. This guards against caching a futures spread against an underlying that was registered as something else (e.g. an index or spot instrument), which would make the spread meaningless.","triggerScenarios":"The underlying symbol+venue resolves to a cached instrument registered as Index/Spot/CurrencyPair instead of Future — e.g. options on an index where the underlying 'ES' was loaded as an index definition, or the underlying ID collides with a non-futures instrument on that venue.","commonSituations":"Loading both an index and a futures product under similar IDs; adapters that classify the underlying as Spot; venue suffix construction (format!(\"{underlying}.{venue}\")) accidentally matching a different listing.","solutions":["Ensure the underlying instrument is added to the Cache with InstrumentClass::Future, or remove the non-future instrument occupying that ID.","Check how underlying_instrument_id is composed (underlying symbol + option venue) and confirm it points at the intended futures contract.","Adjust the adapter's instrument classification so the underlying definition loads as a Future."],"exampleFix":"// before\ncache.add_instrument(index_def)?; // 'SPX.NYSE' registered as Index\n// after\ncache.add_instrument(future_def)?; // ensure the underlying ID maps to a Future instrument","handlingStrategy":"validation","validationCode":"// rust\nlet underlying_id = InstrumentId::from(format!(\"{underlying}.{}\", call_id.venue));\nlet ok = match cache.instrument(&underlying_id) {\n    None => true, // unknown is allowed; only wrong class is rejected\n    Some(i) => i.instrument_class() == InstrumentClass::Future,\n};\nif !ok { tracing::error!(\"{underlying_id} registered as non-future\"); }","typeGuard":"fn is_cached_future(cache: &Cache, id: &InstrumentId) -> bool {\n    cache.instrument(id).is_none_or(|i| i.instrument_class() == InstrumentClass::Future)\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(\"is not a futures contract\") {\n        fix_underlying_classification(&underlying_id);\n    }\n}","preventionTips":["Verify instrument_class at ingestion: the underlying of futures options must load as InstrumentClass::Future.","Avoid registering index/spot definitions under IDs that collide with the futures underlying symbol.","Assert the composed underlying ID (symbol + venue) matches the intended futures contract in integration tests."],"tags":["rust","instrument-class","futures","cache"],"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"}