{"record":{"id":"dfeed941474cd5df","repo":"nautechsystems/nautilus_trader","slug":"cannot-cache-futures-spread-expiration-dates-diff","errorCode":null,"errorMessage":"Cannot cache futures spread: expiration dates differ call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}","messagePattern":"Cannot cache futures spread: expiration dates differ call_instrument_id=(.+?) put_instrument_id=(.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/common/src/greeks.rs","lineNumber":1111,"sourceCode":"            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}\"\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            )","sourceCodeStart":1093,"sourceCodeEnd":1129,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/common/src/greeks.rs#L1093-L1129","documentation":"cache_futures_spread requires both legs to expire at the same time, since the implied future price is computed to a single expiry. If call_instrument.expiration_ns() differs from put_instrument.expiration_ns() the method bails with this error. This catches pairs drawn from different expiries (calendar-style pairs) which parity math cannot handle.","triggerScenarios":"Passing a near-month call with a far-month put; expiry timestamps parsed from different timezones or sources yielding unequal expiration_ns for nominally the same expiry; weekly vs monthly options mixed up.","commonSituations":"Pair-selection code that matches on expiry date strings but the raw ns timestamps differ (e.g. 16:00 vs 09:30 settlement); vendor data with differing expiry conventions per leg.","solutions":["Only pair options with identical expiration_ns (compare the raw timestamps, not formatted dates).","Normalize expiry parsing so both legs resolve to the same expiration_ns value.","Filter the option chain by exact expiration before choosing the call/put pair."],"exampleFix":"// before\nlet price = greeks.cache_futures_spread(may_call_id, jun_put_id, future_id)?;\n// after\nif call.expiration_ns() != put.expiration_ns() {\n    tracing::warn!(\"expiry mismatch, skipping pair\");\n    return Ok(None);\n}\nlet price = greeks.cache_futures_spread(may_call_id, may_put_id, future_id)?;","handlingStrategy":"validation","validationCode":"// rust\nfn same_expiry(cache: &Cache, call_id: &InstrumentId, put_id: &InstrumentId) -> bool {\n    cache.instrument(call_id).and_then(|i| i.expiration_ns()) == cache.instrument(put_id).and_then(|i| i.expiration_ns())\n}","typeGuard":"fn expiry_ns(cache: &Cache, id: &InstrumentId) -> Option<UnixNanos> {\n    cache.instrument(id).and_then(|i| i.expiration_ns())\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(\"expiration dates differ\") => rebuild_chain_for_expiry(expiry),\n    Err(e) => return Err(e),\n}","preventionTips":["Filter the option chain to a single expiration_ns before pairing legs.","Compare raw UnixNanos timestamps, never formatted date strings, when matching expiries.","Normalize timezone/settlement conventions in one ingestion layer so both legs agree."],"tags":["rust","validation","expiration","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"}