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

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 {
normalized: 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 {
@@ -855,7 +868,7 @@ pub struct PlatformExprStrategy {
position_holding_days: BTreeMap<String, i64>,
position_holding_days_last_counted: BTreeMap<String, NaiveDate>,
/// 已编译表达式 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<u64>,
cache_misses: RefCell<u64>,
expression_plan_cache: RefCell<HashMap<String, Arc<ExpressionEvalPlan>>>,
normalized_prelude: String,
prelude_runtime_template: Result<RuntimeExpressionTemplate, String>,
prelude_identifier_candidates: BTreeSet<String>,
prelude_declared_identifiers: BTreeSet<String>,
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("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<Dynamic, BacktestError> {
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<String, BacktestError> {
let mut output = String::with_capacity(expr.len());
fn compile_runtime_helper_template(expr: &str) -> Result<RuntimeExpressionTemplate, String> {
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<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)
}
@@ -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<String, BacktestError> {
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
}