From e3f10286677fe5cc1bb212a008eb5dfb152d17f9 Mon Sep 17 00:00:00 2001 From: boris Date: Wed, 9 Sep 2026 04:34:09 +0800 Subject: [PATCH] fix: reject missing numeric sizing and execution parameters --- .../fidc-core/src/platform_expr_strategy.rs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 59bb088..f5b88f2 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -8363,6 +8363,25 @@ impl PlatformExprStrategy { day: &DayExpressionState, stock: Option<&StockExpressionState>, position: Option<&PositionExpressionState>, + ) -> Result { + let value = self.eval_float_or_missing(ctx, expr, day, stock, position)?; + if !value.is_finite() { + return Err(BacktestError::Execution(format!( + "missing_numeric_result: expression={expr:?}, symbol={}, decision_date={}, execution_date={}", + stock.map(|item| item.symbol.as_ref()).unwrap_or("portfolio"), + ctx.decision_date, ctx.execution_date, + ))); + } + Ok(value) + } + + fn eval_float_or_missing( + &self, + ctx: &StrategyContext<'_>, + expr: &str, + day: &DayExpressionState, + stock: Option<&StockExpressionState>, + position: Option<&PositionExpressionState>, ) -> Result { if let Some(value) = self.eval_numeric_vm(ctx, expr, day, stock, position)? { return match value { @@ -10441,7 +10460,7 @@ impl PlatformExprStrategy { stock: &StockExpressionState, ) -> Result { if self.rank_expr_present { - return match self.eval_float(ctx, &self.config.rank_expr, day, Some(stock), None) { + return match self.eval_float_or_missing(ctx, &self.config.rank_expr, day, Some(stock), None) { Ok(value) => Ok(value), Err(error) if Self::is_missing_rolling_mean_error(&error) => Ok(f64::NAN), Err(error) if Self::is_non_numeric_expr_error(&error) => Ok(f64::NAN), @@ -16644,6 +16663,12 @@ mod tests { assert!(missing_stock.turnover_ratio.is_nan()); assert!(missing_stock.effective_turnover_ratio.is_nan()); assert_eq!(present_stock.turnover_ratio, 0.0); + for expression in ["model_score", "min(model_score, 1.0)", "model_score / 100.0"] { + let error = strategy.eval_float(&ctx, expression, &day, Some(&missing_stock), None).unwrap_err(); + assert!(error.to_string().contains("missing_numeric_result"), "{error}"); + } + assert!(strategy.eval_float_or_missing(&ctx, "model_score", &day, Some(&missing_stock), None).unwrap().is_nan()); + assert_eq!(strategy.eval_float(&ctx, "nz(model_score, 0.0)", &day, Some(&missing_stock), None).unwrap(), 0.0); for predicate in [ "!(model_score > 0.0)", "!(model_score != 0.0)",