fix(loader): soft-validate FrontMonth symbol when no streaming SymbolMapping

Databento historical bulk DBN files emit symbol mappings only in the metadata
header (date-range form), not as in-stream SymbolMappingMsg records. The
strict 'no mapping = error' branch from the previous commit blocked the
front-month smoke (alpha-perception-vstrr) at Q1 2024 where the dominant id
5602 has no streaming mapping entry.

Volume-leader detection itself doesn't depend on the symbol; the regex check
was defense-in-depth. Log-warn on symbol-mismatch and proceed with id=metadata-only
when no streaming mapping exists. Calendar-spread anomalies still surface in
logs without blocking training.
This commit is contained in:
jgrusewski
2026-05-22 18:08:36 +02:00
parent 78a9e08358
commit 20aa345a7c

View File

@@ -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(),