{"record":{"id":"5fa2ef84db0ecefc","repo":"nautechsystems/nautilus_trader","slug":"missing-contract-expiry-for-dated-future-product","errorCode":null,"errorMessage":"Missing contract_expiry for dated future '{product_id}'","messagePattern":"Missing contract_expiry for dated future '(.+?)'","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/adapters/coinbase/src/http/parse.rs","lineNumber":274,"sourceCode":"    let underlying = derive_base_currency(product);\n    let quote_currency = Currency::get_or_create_crypto(product.quote_currency_id);\n    let settlement_currency = quote_currency;\n\n    let price_precision = precision_from_increment(&product.price_increment);\n    let size_precision = precision_from_increment(&product.base_increment);\n\n    let price_increment = parse_price(&product.price_increment, price_precision)?;\n    let size_increment = parse_quantity(&product.base_increment, size_precision)?;\n\n    let min_quantity = parse_optional_quantity(&product.base_min_size);\n    let max_quantity = parse_optional_quantity(&product.base_max_size);\n\n    let expiry_str = product\n        .future_product_details\n        .as_ref()\n        .map_or(\"\", |d| d.contract_expiry.as_str());\n\n    anyhow::ensure!(\n        !expiry_str.is_empty(),\n        \"Missing contract_expiry for dated future '{}'\",\n        product.product_id\n    );\n\n    let expiration_ns = parse_rfc3339_timestamp(expiry_str).context(format!(\n        \"Failed to parse contract_expiry for '{}'\",\n        product.product_id\n    ))?;\n\n    let multiplier = contract_size_multiplier(product);\n\n    let instrument = CryptoFuture::builder()\n        .instrument_id(instrument_id)\n        .raw_symbol(raw_symbol)\n        .underlying(underlying)\n        .quote_currency(quote_currency)\n        .settlement_currency(settlement_currency)","sourceCodeStart":256,"sourceCodeEnd":292,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/adapters/coinbase/src/http/parse.rs#L256-L292","documentation":"Coinbase's INTX futures products expose contract details in future_product_details; a dated future must carry a non-empty contract_expiry string. parse_future_instrument uses it to compute the instrument's expiration_ns, so when it is missing or empty the parser intentionally fails rather than producing an instrument with no expiration. This indicates the product payload for that future lacks the expiry field the adapter requires.","triggerScenarios":"load_all / load_all_filtered / load encounters a product with product_type FUTURE (dated) whose future_product_details is absent or whose contract_expiry is an empty string; calling parse_future_instrument (directly or via parse_instrument) on such a Product.","commonSituations":"Coinbase listing a new future type whose payload shape differs; test fixtures or recorded JSON missing future_product_details; API schema changes where contract_expiry moved or was renamed; expiring/expired products returning empty expiry fields.","solutions":["Check the raw product JSON for the offending product_id and confirm future_product_details.contract_expiry is present and non-empty.","Filter out or skip unsupported product types before parsing (use load_all_filtered with the desired CoinbaseProductType).","Update the Product model/parser if the Coinbase API schema changed the contract_expiry location.","Refresh fixtures/recorded responses if the failure comes from stale test data."],"exampleFix":"// before\nlet instruments = provider.load_all().await?;\n// after (skip non-parseable products / filter to futures that parse)\nlet instruments = provider\n    .load_all_filtered(CoinbaseProductType::Future)\n    .await\n    .unwrap_or_default();\n// or validate before parsing:\nanyhow::ensure!(\n    product.future_product_details.as_ref().map_or(false, |d| !d.contract_expiry.is_empty()),\n    \"product {} lacks contract_expiry; skipping\",\n    product.product_id\n);","handlingStrategy":"validation","validationCode":"fn future_has_expiry(p: &Product) -> bool {\n    p.future_product_details\n        .as_ref()\n        .map_or(false, |d| !d.contract_expiry.is_empty())\n}\nlet instruments: Vec<_> = products.into_iter().filter(|p| p.product_type != CoinbaseProductType::Future || future_has_expiry(p)).collect();","typeGuard":"fn parseable_future(p: &Product) -> bool { p.future_product_details.as_ref().map_or(true, |d| !d.contract_expiry.is_empty()) }","tryCatchPattern":"match parse_instrument(&product) {\n    Ok(i) => cache.insert(i.id(), i),\n    Err(e) => log::warn!(\"skipping product {}: {e:#}\", product.product_id),\n}","preventionTips":["Skip products whose future_product_details.contract_expiry is empty before parsing.","Keep the Product model in sync with Coinbase INTX API changes.","Refresh recorded fixtures regularly and assert required fields exist."],"tags":["instrument-parsing","futures","missing-field","coinbase"],"backgroundTag":"missing-required-config-field","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"}