From 839ca1fa0d741587f3160f1e32cb315eb9e94052 Mon Sep 17 00:00:00 2001 From: boris Date: Sat, 1 Aug 2026 21:51:46 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A2=84=E7=BC=96=E8=AF=91=E7=AD=96=E7=95=A5?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=E8=BE=85=E5=8A=A9=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 146 +++++++++++------- 1 file changed, 92 insertions(+), 54 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index ea4c6d1..2f99a25 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -837,8 +837,21 @@ impl<'a> SelectiveExpressionScope<'a> { } struct ExpressionEvalPlan { - normalized: String, identifiers: BTreeSet, + runtime_template: Result, +} + +struct RuntimeExpressionTemplate { + segments: Vec, +} + +enum RuntimeExpressionSegment { + Literal(String), + Helper { + name: String, + args: Vec, + scope_name: String, + }, } pub struct PlatformExprStrategy { @@ -855,7 +868,7 @@ pub struct PlatformExprStrategy { position_holding_days: BTreeMap, position_holding_days_last_counted: BTreeMap, /// 已编译表达式 AST 缓存。 - /// Key 是经过 normalize/expand_runtime_helpers 之后的完整 script 文本, + /// Key 是经过 normalize/helper template 展开之后的完整 script 文本, /// Value 是 Rhai 编译产物。命中后 eval 走 eval_ast_with_scope,避免重复 /// parsing。一次回测里同一表达式(stock_filter / stop_loss / rank_expr 等) /// 会被反复执行,重复解析的常数级开销在大规模回测里不可忽略。 @@ -864,7 +877,7 @@ pub struct PlatformExprStrategy { cache_hits: RefCell, cache_misses: RefCell, expression_plan_cache: RefCell>>, - normalized_prelude: String, + prelude_runtime_template: Result, prelude_identifier_candidates: BTreeSet, prelude_declared_identifiers: BTreeSet, prelude_numeric_constants: HashMap, @@ -1137,6 +1150,7 @@ impl PlatformExprStrategy { engine.register_fn("strlen", |value: &str| value.chars().count() as i64); engine.register_fn("code_number", code_number_value); let normalized_prelude = Self::normalize_prelude_for_eval(&config.prelude); + let prelude_runtime_template = Self::compile_runtime_helper_template(&normalized_prelude); let prelude_identifier_candidates = Self::extract_identifier_candidates(&normalized_prelude); let prelude_declared_identifiers = Self::declared_prelude_identifiers(&config.prelude); @@ -1185,7 +1199,7 @@ impl PlatformExprStrategy { cache_hits: RefCell::new(0), cache_misses: RefCell::new(0), expression_plan_cache: RefCell::new(HashMap::new()), - normalized_prelude, + prelude_runtime_template, prelude_identifier_candidates, prelude_declared_identifiers, prelude_numeric_constants, @@ -4462,7 +4476,6 @@ impl PlatformExprStrategy { position: Option<&PositionExpressionState>, ) -> Result { let expression_plan = self.expression_eval_plan(expr); - let normalized_expr = expression_plan.normalized.as_str(); let normalized_identifiers = &expression_plan.identifiers; let prelude_identifiers = &self.prelude_identifier_candidates; let include_day_factors = normalized_identifiers.contains("day_factors") @@ -4485,10 +4498,20 @@ impl PlatformExprStrategy { include_factors_map, include_process_event_counts, ); - let expanded_prelude = - self.expand_runtime_helpers(ctx, day, stock, &self.normalized_prelude, &mut scope)?; - let expanded_expr = - self.expand_runtime_helpers(ctx, day, stock, &normalized_expr, &mut scope)?; + let expanded_prelude = self.expand_runtime_helper_template( + ctx, + day, + stock, + &self.prelude_runtime_template, + &mut scope, + )?; + let expanded_expr = self.expand_runtime_helper_template( + ctx, + day, + stock, + &expression_plan.runtime_template, + &mut scope, + )?; if let Some(item) = stock { let factor_identifiers = normalized_identifiers.iter().chain( prelude_identifiers @@ -4533,7 +4556,7 @@ impl PlatformExprStrategy { let normalized = Self::normalize_expr(expr); let plan = Arc::new(ExpressionEvalPlan { identifiers: Self::extract_identifier_candidates(&normalized), - normalized, + runtime_template: Self::compile_runtime_helper_template(&normalized), }); self.expression_plan_cache .borrow_mut() @@ -4873,15 +4896,9 @@ impl PlatformExprStrategy { output } - fn expand_runtime_helpers( - &self, - ctx: &StrategyContext<'_>, - day: &DayExpressionState, - stock: Option<&StockExpressionState>, - expr: &str, - scope: &mut Scope<'_>, - ) -> Result { - let mut output = String::with_capacity(expr.len()); + fn compile_runtime_helper_template(expr: &str) -> Result { + let mut segments = Vec::new(); + let mut literal_start = 0usize; let mut cursor = 0usize; let mut in_single_quote = false; let mut in_double_quote = false; @@ -4891,36 +4908,26 @@ impl PlatformExprStrategy { break; }; if escaped { - output.push(ch); escaped = false; cursor += ch.len_utf8(); continue; } if ch == '\\' && (in_single_quote || in_double_quote) { - output.push(ch); escaped = true; cursor += ch.len_utf8(); continue; } if ch == '\'' && !in_double_quote { - output.push(ch); in_single_quote = !in_single_quote; cursor += ch.len_utf8(); continue; } if ch == '"' && !in_single_quote { - output.push(ch); in_double_quote = !in_double_quote; cursor += ch.len_utf8(); continue; } - if in_single_quote || in_double_quote { - output.push(ch); - cursor += ch.len_utf8(); - continue; - } - if !(ch == '_' || ch.is_ascii_alphabetic()) { - output.push(ch); + if in_single_quote || in_double_quote || !(ch == '_' || ch.is_ascii_alphabetic()) { cursor += ch.len_utf8(); continue; } @@ -4937,7 +4944,6 @@ impl PlatformExprStrategy { } } let ident = &expr[ident_start..cursor]; - let whitespace_start = cursor; while cursor < expr.len() { let Some(next) = expr[cursor..].chars().next() else { break; @@ -4948,25 +4954,61 @@ impl PlatformExprStrategy { break; } } - let Some(next) = expr[cursor..].chars().next() else { - output.push_str(&expr[ident_start..cursor]); - break; - }; - if next != '(' || !Self::is_runtime_helper(ident) { - output.push_str(&expr[ident_start..cursor]); + let next_is_call = expr[cursor..].starts_with('('); + if !next_is_call || !Self::is_runtime_helper(ident) { continue; } let Some(close_idx) = Self::find_matching_paren(expr, cursor) else { - return Err(BacktestError::Execution(format!( + return Err(format!( "platform helper call not closed: {}", &expr[ident_start..] - ))); + )); }; - let inner = &expr[cursor + 1..close_idx]; - let replacement = self.resolve_runtime_helper(ctx, day, stock, ident, inner, scope)?; - output.push_str(&replacement); + if literal_start < ident_start { + segments.push(RuntimeExpressionSegment::Literal( + expr[literal_start..ident_start].to_string(), + )); + } + let args = Self::split_top_level_args(&expr[cursor + 1..close_idx]); + segments.push(RuntimeExpressionSegment::Helper { + name: ident.to_string(), + scope_name: Self::runtime_helper_scope_name(ident, &args), + args, + }); cursor = close_idx + 1; - let _ = whitespace_start; + literal_start = cursor; + } + if literal_start < expr.len() { + segments.push(RuntimeExpressionSegment::Literal( + expr[literal_start..].to_string(), + )); + } + Ok(RuntimeExpressionTemplate { segments }) + } + + fn expand_runtime_helper_template( + &self, + ctx: &StrategyContext<'_>, + day: &DayExpressionState, + stock: Option<&StockExpressionState>, + template: &Result, + scope: &mut Scope<'_>, + ) -> Result { + let template = template + .as_ref() + .map_err(|error| BacktestError::Execution(error.clone()))?; + let mut output = String::new(); + for segment in &template.segments { + match segment { + RuntimeExpressionSegment::Literal(literal) => output.push_str(literal), + RuntimeExpressionSegment::Helper { + name, + args, + scope_name, + } => output.push_str( + &self.resolve_runtime_helper(ctx, day, stock, name, args, scope_name, scope)?, + ), + } } Ok(output) } @@ -4977,10 +5019,10 @@ impl PlatformExprStrategy { day: &DayExpressionState, stock: Option<&StockExpressionState>, helper: &str, - args_src: &str, + args: &[String], + scope_name: &str, scope: &mut Scope<'_>, ) -> Result { - let args = Self::split_top_level_args(args_src); match helper { "factor" => { let key = Self::normalize_runtime_factor_key(&Self::parse_string_or_identifier( @@ -5005,8 +5047,7 @@ impl PlatformExprStrategy { let value = self.resolve_rolling_mean(ctx, day, stock, &field, lookback)?; Ok(Self::push_runtime_helper_value( scope, - helper, - &args, + scope_name, Dynamic::from(value), )) } @@ -5021,8 +5062,7 @@ impl PlatformExprStrategy { let value = self.resolve_current_rolling_mean(ctx, day, stock, &field, lookback)?; Ok(Self::push_runtime_helper_value( scope, - helper, - &args, + scope_name, Dynamic::from(value), )) } @@ -5075,8 +5115,7 @@ impl PlatformExprStrategy { let value = self.resolve_rolling_mean(ctx, day, stock, "volume", lookback)?; Ok(Self::push_runtime_helper_value( scope, - helper, - &args, + scope_name, Dynamic::from(value), )) } @@ -5551,11 +5590,10 @@ impl PlatformExprStrategy { fn push_runtime_helper_value( scope: &mut Scope<'_>, - helper: &str, - args: &[String], + scope_name: &str, value: Dynamic, ) -> String { - let name = Self::runtime_helper_scope_name(helper, args); + let name = scope_name.to_string(); scope.push_dynamic(name.clone(), value); name }