fix: fail closed on missing factor values

This commit is contained in:
boris
2026-08-31 14:30:33 +08:00
parent dff791b51f
commit d014bb2fbd
2 changed files with 59 additions and 2 deletions
+19
View File
@@ -4907,6 +4907,25 @@ mod tests {
assert!(data.factor_by_symbol_id(date, signal_id).is_none()); assert!(data.factor_by_symbol_id(date, signal_id).is_none());
assert!(data.candidate_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()); 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<_>>(),
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] #[test]
+40 -2
View File
@@ -5077,7 +5077,18 @@ impl PlatformExprStrategy {
.get_factor(&stock.symbol, start, day.date, field) .get_factor(&stock.symbol, start, day.date, field)
.last() .last()
.map(|row| row.value) .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)) Ok(Self::normalized_runtime_number(value))
} }
} }
@@ -6455,7 +6466,18 @@ impl PlatformExprStrategy {
.get_factor(&stock.symbol, start, day.date, &field) .get_factor(&stock.symbol, start, day.date, &field)
.last() .last()
.map(|row| row.value) .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)) Ok(Self::normalized_runtime_number(value))
} }
"factor_text" | "get_factor_text" => { "factor_text" | "get_factor_text" => {
@@ -14311,6 +14333,22 @@ mod tests {
.stock_passes_expr(&ctx, &map_day, &map_stock) .stock_passes_expr(&ctx, &map_day, &map_stock)
.expect("factor map filter") .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] #[test]