预编译策略运行时辅助函数
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user