From 8d0bc0805e0a0410d713fba518cc93a3a20c2cd8 Mon Sep 17 00:00:00 2001 From: boris Date: Sat, 29 Aug 2026 19:44:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9C=A8=E8=A1=A8=E8=BE=BE=E5=BC=8F=E9=A2=84?= =?UTF-8?q?=E6=A3=80=E9=98=B6=E6=AE=B5=E6=A0=A1=E9=AA=8C=E5=80=BC=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index e55f257..21f6da6 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -1348,6 +1348,7 @@ impl PlatformExprStrategy { /// This validates syntax only; identifiers and runtime values are resolved /// later against the point-in-time execution scope. pub fn validate_expression_syntax(&self) -> Result<(), BacktestError> { + self.validate_expression_value_kinds()?; let normalized_prelude = Self::normalize_prelude_for_eval(&self.config.prelude); let mut expressions = vec![ ( @@ -1565,6 +1566,88 @@ impl PlatformExprStrategy { Ok(()) } + fn trim_outer_parentheses_for_expression_literal(expression: &str) -> &str { + let bytes = expression.as_bytes(); + let mut start = 0usize; + let mut end = bytes.len(); + while start < end && bytes[start].is_ascii_whitespace() { + start += 1; + } + while end > start && bytes[end - 1].is_ascii_whitespace() { + end -= 1; + } + loop { + if end <= start + 1 || bytes[start] != b'(' || bytes[end - 1] != b')' { + break; + } + start += 1; + end -= 1; + while start < end && bytes[start].is_ascii_whitespace() { + start += 1; + } + while end > start && bytes[end - 1].is_ascii_whitespace() { + end -= 1; + } + } + &expression[start..end] + } + + fn is_single_string_literal_expression(expression: &str) -> bool { + let expression = Self::trim_outer_parentheses_for_expression_literal(expression).trim(); + let Some(quote) = expression.chars().next() else { + return false; + }; + if quote != '"' && quote != '\'' { + return false; + } + let mut escaped = false; + for (index, character) in expression.char_indices().skip(1) { + if escaped { + escaped = false; + continue; + } + if character == '\\' { + escaped = true; + continue; + } + if character == quote { + return expression[index + character.len_utf8()..].trim().is_empty(); + } + } + false + } + + fn validate_expression_value_kinds(&self) -> Result<(), BacktestError> { + if Self::is_single_string_literal_expression(&self.config.stock_filter_expr) { + return Err(BacktestError::Execution( + "field=stock_filter_expr must be a boolean expression; received a string literal" + .to_string(), + )); + } + for (field, expression) in [ + ("refresh_rate_expr", self.config.refresh_rate_expr.as_str()), + ("market_cap_lower_expr", self.config.market_cap_lower_expr.as_str()), + ("market_cap_upper_expr", self.config.market_cap_upper_expr.as_str()), + ("selection_limit_expr", self.config.selection_limit_expr.as_str()), + ( + "selection_candidate_limit_expr", + self.config.selection_candidate_limit_expr.as_str(), + ), + ("buy_scale_expr", self.config.buy_scale_expr.as_str()), + ("exposure_expr", self.config.exposure_expr.as_str()), + ("stop_loss_expr", self.config.stop_loss_expr.as_str()), + ("take_profit_expr", self.config.take_profit_expr.as_str()), + ("rank_expr", self.config.rank_expr.as_str()), + ] { + if Self::is_single_string_literal_expression(expression) { + return Err(BacktestError::Execution(format!( + "field={field} must be a numeric expression; received a string literal" + ))); + } + } + Ok(()) + } + /// 用 AST 缓存执行 script。命中:直接走 eval_ast_with_scope;未命中:先 /// engine.compile,再插入缓存,再 eval_ast_with_scope。任何编译/执行错误 /// 都按字符串包装为 BacktestError::Execution。