预编译策略运行时辅助函数

This commit is contained in:
boris
2026-08-01 21:51:46 +08:00
parent d51d324977
commit 839ca1fa0d
+92 -54
View File
@@ -837,8 +837,21 @@ impl<'a> SelectiveExpressionScope<'a> {
} }
struct ExpressionEvalPlan { struct ExpressionEvalPlan {
normalized: String,
identifiers: BTreeSet<String>, identifiers: BTreeSet<String>,
runtime_template: Result<RuntimeExpressionTemplate, String>,
}
struct RuntimeExpressionTemplate {
segments: Vec<RuntimeExpressionSegment>,
}
enum RuntimeExpressionSegment {
Literal(String),
Helper {
name: String,
args: Vec<String>,
scope_name: String,
},
} }
pub struct PlatformExprStrategy { pub struct PlatformExprStrategy {
@@ -855,7 +868,7 @@ pub struct PlatformExprStrategy {
position_holding_days: BTreeMap<String, i64>, position_holding_days: BTreeMap<String, i64>,
position_holding_days_last_counted: BTreeMap<String, NaiveDate>, position_holding_days_last_counted: BTreeMap<String, NaiveDate>,
/// 已编译表达式 AST 缓存。 /// 已编译表达式 AST 缓存。
/// Key 是经过 normalize/expand_runtime_helpers 之后的完整 script 文本, /// Key 是经过 normalize/helper template 展开之后的完整 script 文本,
/// Value 是 Rhai 编译产物。命中后 eval 走 eval_ast_with_scope,避免重复 /// Value 是 Rhai 编译产物。命中后 eval 走 eval_ast_with_scope,避免重复
/// parsing。一次回测里同一表达式(stock_filter / stop_loss / rank_expr 等) /// parsing。一次回测里同一表达式(stock_filter / stop_loss / rank_expr 等)
/// 会被反复执行,重复解析的常数级开销在大规模回测里不可忽略。 /// 会被反复执行,重复解析的常数级开销在大规模回测里不可忽略。
@@ -864,7 +877,7 @@ pub struct PlatformExprStrategy {
cache_hits: RefCell<u64>, cache_hits: RefCell<u64>,
cache_misses: RefCell<u64>, cache_misses: RefCell<u64>,
expression_plan_cache: RefCell<HashMap<String, Arc<ExpressionEvalPlan>>>, expression_plan_cache: RefCell<HashMap<String, Arc<ExpressionEvalPlan>>>,
normalized_prelude: String, prelude_runtime_template: Result<RuntimeExpressionTemplate, String>,
prelude_identifier_candidates: BTreeSet<String>, prelude_identifier_candidates: BTreeSet<String>,
prelude_declared_identifiers: BTreeSet<String>, prelude_declared_identifiers: BTreeSet<String>,
prelude_numeric_constants: HashMap<String, f64>, prelude_numeric_constants: HashMap<String, f64>,
@@ -1137,6 +1150,7 @@ impl PlatformExprStrategy {
engine.register_fn("strlen", |value: &str| value.chars().count() as i64); engine.register_fn("strlen", |value: &str| value.chars().count() as i64);
engine.register_fn("code_number", code_number_value); engine.register_fn("code_number", code_number_value);
let normalized_prelude = Self::normalize_prelude_for_eval(&config.prelude); 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 = let prelude_identifier_candidates =
Self::extract_identifier_candidates(&normalized_prelude); Self::extract_identifier_candidates(&normalized_prelude);
let prelude_declared_identifiers = Self::declared_prelude_identifiers(&config.prelude); let prelude_declared_identifiers = Self::declared_prelude_identifiers(&config.prelude);
@@ -1185,7 +1199,7 @@ impl PlatformExprStrategy {
cache_hits: RefCell::new(0), cache_hits: RefCell::new(0),
cache_misses: RefCell::new(0), cache_misses: RefCell::new(0),
expression_plan_cache: RefCell::new(HashMap::new()), expression_plan_cache: RefCell::new(HashMap::new()),
normalized_prelude, prelude_runtime_template,
prelude_identifier_candidates, prelude_identifier_candidates,
prelude_declared_identifiers, prelude_declared_identifiers,
prelude_numeric_constants, prelude_numeric_constants,
@@ -4462,7 +4476,6 @@ impl PlatformExprStrategy {
position: Option<&PositionExpressionState>, position: Option<&PositionExpressionState>,
) -> Result<Dynamic, BacktestError> { ) -> Result<Dynamic, BacktestError> {
let expression_plan = self.expression_eval_plan(expr); let expression_plan = self.expression_eval_plan(expr);
let normalized_expr = expression_plan.normalized.as_str();
let normalized_identifiers = &expression_plan.identifiers; let normalized_identifiers = &expression_plan.identifiers;
let prelude_identifiers = &self.prelude_identifier_candidates; let prelude_identifiers = &self.prelude_identifier_candidates;
let include_day_factors = normalized_identifiers.contains("day_factors") let include_day_factors = normalized_identifiers.contains("day_factors")
@@ -4485,10 +4498,20 @@ impl PlatformExprStrategy {
include_factors_map, include_factors_map,
include_process_event_counts, include_process_event_counts,
); );
let expanded_prelude = let expanded_prelude = self.expand_runtime_helper_template(
self.expand_runtime_helpers(ctx, day, stock, &self.normalized_prelude, &mut scope)?; ctx,
let expanded_expr = day,
self.expand_runtime_helpers(ctx, day, stock, &normalized_expr, &mut scope)?; 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 { if let Some(item) = stock {
let factor_identifiers = normalized_identifiers.iter().chain( let factor_identifiers = normalized_identifiers.iter().chain(
prelude_identifiers prelude_identifiers
@@ -4533,7 +4556,7 @@ impl PlatformExprStrategy {
let normalized = Self::normalize_expr(expr); let normalized = Self::normalize_expr(expr);
let plan = Arc::new(ExpressionEvalPlan { let plan = Arc::new(ExpressionEvalPlan {
identifiers: Self::extract_identifier_candidates(&normalized), identifiers: Self::extract_identifier_candidates(&normalized),
normalized, runtime_template: Self::compile_runtime_helper_template(&normalized),
}); });
self.expression_plan_cache self.expression_plan_cache
.borrow_mut() .borrow_mut()
@@ -4873,15 +4896,9 @@ impl PlatformExprStrategy {
output output
} }
fn expand_runtime_helpers( fn compile_runtime_helper_template(expr: &str) -> Result<RuntimeExpressionTemplate, String> {
&self, let mut segments = Vec::new();
ctx: &StrategyContext<'_>, let mut literal_start = 0usize;
day: &DayExpressionState,
stock: Option<&StockExpressionState>,
expr: &str,
scope: &mut Scope<'_>,
) -> Result<String, BacktestError> {
let mut output = String::with_capacity(expr.len());
let mut cursor = 0usize; let mut cursor = 0usize;
let mut in_single_quote = false; let mut in_single_quote = false;
let mut in_double_quote = false; let mut in_double_quote = false;
@@ -4891,36 +4908,26 @@ impl PlatformExprStrategy {
break; break;
}; };
if escaped { if escaped {
output.push(ch);
escaped = false; escaped = false;
cursor += ch.len_utf8(); cursor += ch.len_utf8();
continue; continue;
} }
if ch == '\\' && (in_single_quote || in_double_quote) { if ch == '\\' && (in_single_quote || in_double_quote) {
output.push(ch);
escaped = true; escaped = true;
cursor += ch.len_utf8(); cursor += ch.len_utf8();
continue; continue;
} }
if ch == '\'' && !in_double_quote { if ch == '\'' && !in_double_quote {
output.push(ch);
in_single_quote = !in_single_quote; in_single_quote = !in_single_quote;
cursor += ch.len_utf8(); cursor += ch.len_utf8();
continue; continue;
} }
if ch == '"' && !in_single_quote { if ch == '"' && !in_single_quote {
output.push(ch);
in_double_quote = !in_double_quote; in_double_quote = !in_double_quote;
cursor += ch.len_utf8(); cursor += ch.len_utf8();
continue; continue;
} }
if in_single_quote || in_double_quote { if in_single_quote || in_double_quote || !(ch == '_' || ch.is_ascii_alphabetic()) {
output.push(ch);
cursor += ch.len_utf8();
continue;
}
if !(ch == '_' || ch.is_ascii_alphabetic()) {
output.push(ch);
cursor += ch.len_utf8(); cursor += ch.len_utf8();
continue; continue;
} }
@@ -4937,7 +4944,6 @@ impl PlatformExprStrategy {
} }
} }
let ident = &expr[ident_start..cursor]; let ident = &expr[ident_start..cursor];
let whitespace_start = cursor;
while cursor < expr.len() { while cursor < expr.len() {
let Some(next) = expr[cursor..].chars().next() else { let Some(next) = expr[cursor..].chars().next() else {
break; break;
@@ -4948,25 +4954,61 @@ impl PlatformExprStrategy {
break; break;
} }
} }
let Some(next) = expr[cursor..].chars().next() else { let next_is_call = expr[cursor..].starts_with('(');
output.push_str(&expr[ident_start..cursor]); if !next_is_call || !Self::is_runtime_helper(ident) {
break;
};
if next != '(' || !Self::is_runtime_helper(ident) {
output.push_str(&expr[ident_start..cursor]);
continue; continue;
} }
let Some(close_idx) = Self::find_matching_paren(expr, cursor) else { let Some(close_idx) = Self::find_matching_paren(expr, cursor) else {
return Err(BacktestError::Execution(format!( return Err(format!(
"platform helper call not closed: {}", "platform helper call not closed: {}",
&expr[ident_start..] &expr[ident_start..]
))); ));
}; };
let inner = &expr[cursor + 1..close_idx]; if literal_start < ident_start {
let replacement = self.resolve_runtime_helper(ctx, day, stock, ident, inner, scope)?; segments.push(RuntimeExpressionSegment::Literal(
output.push_str(&replacement); 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; 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<RuntimeExpressionTemplate, String>,
scope: &mut Scope<'_>,
) -> Result<String, BacktestError> {
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) Ok(output)
} }
@@ -4977,10 +5019,10 @@ impl PlatformExprStrategy {
day: &DayExpressionState, day: &DayExpressionState,
stock: Option<&StockExpressionState>, stock: Option<&StockExpressionState>,
helper: &str, helper: &str,
args_src: &str, args: &[String],
scope_name: &str,
scope: &mut Scope<'_>, scope: &mut Scope<'_>,
) -> Result<String, BacktestError> { ) -> Result<String, BacktestError> {
let args = Self::split_top_level_args(args_src);
match helper { match helper {
"factor" => { "factor" => {
let key = Self::normalize_runtime_factor_key(&Self::parse_string_or_identifier( 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)?; let value = self.resolve_rolling_mean(ctx, day, stock, &field, lookback)?;
Ok(Self::push_runtime_helper_value( Ok(Self::push_runtime_helper_value(
scope, scope,
helper, scope_name,
&args,
Dynamic::from(value), Dynamic::from(value),
)) ))
} }
@@ -5021,8 +5062,7 @@ impl PlatformExprStrategy {
let value = self.resolve_current_rolling_mean(ctx, day, stock, &field, lookback)?; let value = self.resolve_current_rolling_mean(ctx, day, stock, &field, lookback)?;
Ok(Self::push_runtime_helper_value( Ok(Self::push_runtime_helper_value(
scope, scope,
helper, scope_name,
&args,
Dynamic::from(value), Dynamic::from(value),
)) ))
} }
@@ -5075,8 +5115,7 @@ impl PlatformExprStrategy {
let value = self.resolve_rolling_mean(ctx, day, stock, "volume", lookback)?; let value = self.resolve_rolling_mean(ctx, day, stock, "volume", lookback)?;
Ok(Self::push_runtime_helper_value( Ok(Self::push_runtime_helper_value(
scope, scope,
helper, scope_name,
&args,
Dynamic::from(value), Dynamic::from(value),
)) ))
} }
@@ -5551,11 +5590,10 @@ impl PlatformExprStrategy {
fn push_runtime_helper_value( fn push_runtime_helper_value(
scope: &mut Scope<'_>, scope: &mut Scope<'_>,
helper: &str, scope_name: &str,
args: &[String],
value: Dynamic, value: Dynamic,
) -> String { ) -> String {
let name = Self::runtime_helper_scope_name(helper, args); let name = scope_name.to_string();
scope.push_dynamic(name.clone(), value); scope.push_dynamic(name.clone(), value);
name name
} }