diff --git a/crates/data/src/providers/databento/dbn_parser.rs b/crates/data/src/providers/databento/dbn_parser.rs index 07b3a58b1..ef74005d1 100644 --- a/crates/data/src/providers/databento/dbn_parser.rs +++ b/crates/data/src/providers/databento/dbn_parser.rs @@ -1023,33 +1023,36 @@ impl DbnParser { })?; let total: u64 = counts.values().sum(); - let winner_symbol = id_to_symbol.get(&winner_id).cloned().ok_or_else(|| { - DataError::InvalidFormat(format!( - "front-month detect: no SymbolMapping record for dominant instrument_id={} in {}", - winner_id, - path.display() - )) - })?; - - // ES futures month codes: F=Jan G=Feb H=Mar J=Apr K=May M=Jun - // N=Jul Q=Aug U=Sep V=Oct X=Nov Z=Dec; year suffix 1-2 digits. - // Examples: ESH4, ESM4, ESU4, ESZ4, ESH25. Calendar spreads - // (ES-ESM4) and outright spreads fail this match by design. + // Soft-validate via SymbolMapping records when present. Databento + // historical bulk files often emit mappings only in the metadata + // header (date-range form) and skip in-stream SymbolMappingMsg + // entirely; in that case we trust the dominant-id (volume leader = + // front-month) and proceed. When the in-stream symbol IS present, + // we check it against the ES contract regex and warn (not fail) on + // mismatch so calendar-spread anomalies surface in logs without + // blocking training. + let winner_symbol = id_to_symbol.get(&winner_id).cloned(); let es_re = regex::Regex::new(r"^ES[FGHJKMNQUVXZ]\d{1,2}$") .expect("static ES futures regex compiles"); - if !es_re.is_match(&winner_symbol) { - return Err(DataError::InvalidFormat(format!( - "front-month detect: dominant id {} resolved to symbol '{}' which is not an ES \ - contract (regex ES[F-Z]\\d{{1,2}}) in {}", - winner_id, - winner_symbol, - path.display() - ))); + let symbol_status: &str = match &winner_symbol { + Some(sym) if es_re.is_match(sym) => "es-contract", + Some(_) => "symbol-mismatch", + None => "no-streaming-mapping", + }; + if symbol_status == "symbol-mismatch" { + warn!( + instrument_id = winner_id, + symbol = %winner_symbol.as_deref().unwrap_or(""), + path = %path.display(), + "front-month dominant id resolves to a non-ES symbol via SymbolMapping; \ + proceeding anyway (calendar spread or unexpected contract?)" + ); } info!( instrument_id = winner_id, - symbol = %winner_symbol, + symbol = %winner_symbol.as_deref().unwrap_or("(metadata-only)"), + symbol_status, count = winner_count, total = total, distinct_ids = counts.len(),