From d014bb2fbdebb9d7374af15f3e95cb9bc5673fb0 Mon Sep 17 00:00:00 2001 From: boris Date: Mon, 31 Aug 2026 14:30:33 +0800 Subject: [PATCH] fix: fail closed on missing factor values --- crates/fidc-core/src/data.rs | 19 +++++++++ .../fidc-core/src/platform_expr_strategy.rs | 42 ++++++++++++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index e6c5768..b4f93f8 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -4907,6 +4907,25 @@ mod tests { assert!(data.factor_by_symbol_id(date, signal_id).is_none()); assert!(data.candidate_by_symbol_id(date, signal_id).is_none()); assert!(day.candidate(signal_id).is_none()); + + // `get_factor` must use the same symbol-id index as direct snapshot + // lookups. Sparse factor rows must not accidentally select another + // symbol's row or disappear when the date group contains gaps. + let market_cap = data.get_factor("000001.SZ", date, date, "MARKET_CAP"); + assert_eq!( + market_cap + .iter() + .map(|row| (row.date, row.symbol.as_str(), row.value)) + .collect::>(), + vec![(date, "000001.SZ", 100.0)] + ); + assert!( + data.get_factor("000300.SH", date, date, "market_cap") + .is_empty() + ); + assert!(data + .get_factor("999999.SZ", date, date, "market_cap") + .is_empty()); } #[test] diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 4cbb90e..b622eae 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -5077,7 +5077,18 @@ impl PlatformExprStrategy { .get_factor(&stock.symbol, start, day.date, field) .last() .map(|row| row.value) - .unwrap_or(0.0); + .ok_or_else(|| { + BacktestError::Execution(format!( + "missing factor value: field={field}, symbol={}, date={}", + stock.symbol, day.date + )) + })?; + if !value.is_finite() { + return Err(BacktestError::Execution(format!( + "invalid factor value: field={field}, symbol={}, date={}, value={value:?}", + stock.symbol, day.date + ))); + } Ok(Self::normalized_runtime_number(value)) } } @@ -6455,7 +6466,18 @@ impl PlatformExprStrategy { .get_factor(&stock.symbol, start, day.date, &field) .last() .map(|row| row.value) - .unwrap_or(0.0); + .ok_or_else(|| { + BacktestError::Execution(format!( + "missing factor value: field={field}, symbol={}, date={}", + stock.symbol, day.date + )) + })?; + if !value.is_finite() { + return Err(BacktestError::Execution(format!( + "invalid factor value: field={field}, symbol={}, date={}, value={value:?}", + stock.symbol, day.date + ))); + } Ok(Self::normalized_runtime_number(value)) } "factor_text" | "get_factor_text" => { @@ -14311,6 +14333,22 @@ mod tests { .stock_passes_expr(&ctx, &map_day, &map_stock) .expect("factor map filter") ); + + let missing_factor_error = map_strategy + .resolve_runtime_helper( + &ctx, + &map_day, + Some(&missing_stock), + "factor_value", + &["\"missing_factor\"".to_string()], + ) + .expect_err("missing factor_value must fail closed"); + assert!( + missing_factor_error + .to_string() + .contains("missing factor value"), + "unexpected error: {missing_factor_error}" + ); } #[test]