前置校验策略表达式语法

This commit is contained in:
boris
2026-08-23 22:58:33 +08:00
parent 7afb72dca8
commit 9a7e5c7903
@@ -1230,6 +1230,180 @@ impl PlatformExprStrategy {
self.compiled_cache.borrow().len() self.compiled_cache.borrow().len()
} }
/// Compile every configured expression before any market data is loaded.
/// 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> {
let normalized_prelude = Self::normalize_prelude_for_eval(&self.config.prelude);
let mut expressions = vec![
(
"refresh_rate_expr".to_string(),
self.config.refresh_rate_expr.as_str(),
),
(
"market_cap_lower_expr".to_string(),
self.config.market_cap_lower_expr.as_str(),
),
(
"market_cap_upper_expr".to_string(),
self.config.market_cap_upper_expr.as_str(),
),
(
"selection_limit_expr".to_string(),
self.config.selection_limit_expr.as_str(),
),
(
"selection_candidate_limit_expr".to_string(),
self.config.selection_candidate_limit_expr.as_str(),
),
(
"stock_filter_expr".to_string(),
self.config.stock_filter_expr.as_str(),
),
(
"buy_scale_expr".to_string(),
self.config.buy_scale_expr.as_str(),
),
(
"exposure_expr".to_string(),
self.config.exposure_expr.as_str(),
),
(
"stop_loss_expr".to_string(),
self.config.stop_loss_expr.as_str(),
),
(
"take_profit_expr".to_string(),
self.config.take_profit_expr.as_str(),
),
("rank_expr".to_string(), self.config.rank_expr.as_str()),
];
for (index, action) in self.config.explicit_actions.iter().enumerate() {
match action {
PlatformTradeAction::Order {
amount_expr,
limit_price_expr,
start_time_expr,
end_time_expr,
when_expr,
..
} => {
expressions.push((
format!("explicit_actions[{index}].amount_expr"),
amount_expr,
));
for (name, expression) in [
("limit_price_expr", limit_price_expr.as_deref()),
("start_time_expr", start_time_expr.as_deref()),
("end_time_expr", end_time_expr.as_deref()),
("when_expr", when_expr.as_deref()),
] {
if let Some(expression) = expression {
expressions
.push((format!("explicit_actions[{index}].{name}"), expression));
}
}
}
PlatformTradeAction::TargetPortfolioSmart {
target_weights_expr,
order_prices_expr,
valuation_prices_expr,
when_expr,
..
} => {
expressions.push((
format!("explicit_actions[{index}].target_weights_expr"),
target_weights_expr,
));
for (name, expression) in [
("order_prices_expr", order_prices_expr.as_deref()),
("valuation_prices_expr", valuation_prices_expr.as_deref()),
("when_expr", when_expr.as_deref()),
] {
if let Some(expression) = expression {
expressions
.push((format!("explicit_actions[{index}].{name}"), expression));
}
}
}
PlatformTradeAction::Universe {
symbols_expr,
when_expr,
..
} => {
expressions.push((
format!("explicit_actions[{index}].symbols_expr"),
symbols_expr,
));
if let Some(expression) = when_expr.as_deref() {
expressions
.push((format!("explicit_actions[{index}].when_expr"), expression));
}
}
PlatformTradeAction::Account {
amount_expr,
receiving_days_expr,
when_expr,
..
} => {
expressions.push((
format!("explicit_actions[{index}].amount_expr"),
amount_expr,
));
for (name, expression) in [
("receiving_days_expr", receiving_days_expr.as_deref()),
("when_expr", when_expr.as_deref()),
] {
if let Some(expression) = expression {
expressions
.push((format!("explicit_actions[{index}].{name}"), expression));
}
}
}
PlatformTradeAction::Cancel {
order_id_expr,
when_expr,
..
} => {
for (name, expression) in [
("order_id_expr", order_id_expr.as_deref()),
("when_expr", when_expr.as_deref()),
] {
if let Some(expression) = expression {
expressions
.push((format!("explicit_actions[{index}].{name}"), expression));
}
}
}
}
}
if !normalized_prelude.trim().is_empty() {
self.engine.compile(&normalized_prelude).map_err(|error| {
BacktestError::Execution(format!(
"platform expr preflight compile failed field=prelude: {error}"
))
})?;
}
for (field, expression) in expressions {
if expression.trim().is_empty() {
continue;
}
let normalized_expression = Self::normalize_expr(expression);
let script = if normalized_prelude.trim().is_empty() {
normalized_expression
} else {
format!("{normalized_prelude}\n{normalized_expression}")
};
self.engine.compile(&script).map_err(|error| {
BacktestError::Execution(format!(
"platform expr preflight compile failed field={field}: {error}"
))
})?;
}
Ok(())
}
/// 用 AST 缓存执行 script。命中:直接走 eval_ast_with_scope;未命中:先 /// 用 AST 缓存执行 script。命中:直接走 eval_ast_with_scope;未命中:先
/// engine.compile,再插入缓存,再 eval_ast_with_scope。任何编译/执行错误 /// engine.compile,再插入缓存,再 eval_ast_with_scope。任何编译/执行错误
/// 都按字符串包装为 BacktestError::Execution。 /// 都按字符串包装为 BacktestError::Execution。
@@ -11211,6 +11385,49 @@ mod tests {
}); });
} }
#[test]
fn platform_expr_preflight_accepts_all_configured_expression_categories() {
let mut config = PlatformExprStrategyConfig::microcap_rotation();
config.refresh_rate_expr = "max(1, 5)".to_string();
config.rank_expr = "market_cap + 1.0".to_string();
config.explicit_actions = vec![
PlatformTradeAction::Order {
kind: PlatformExplicitOrderKind::LimitValue,
symbol: "000001.SZ".to_string(),
amount_expr: "cash * 0.1".to_string(),
limit_price_expr: Some("close * 1.01".to_string()),
start_time_expr: Some("\"10:00\"".to_string()),
end_time_expr: Some("\"10:30\"".to_string()),
when_expr: Some("close > 0.0".to_string()),
reason: "preflight".to_string(),
},
PlatformTradeAction::Cancel {
kind: PlatformExplicitCancelKind::Order,
symbol: None,
order_id_expr: Some("1".to_string()),
when_expr: Some("true".to_string()),
reason: "preflight".to_string(),
},
];
PlatformExprStrategy::new(config)
.validate_expression_syntax()
.expect("configured expressions should compile");
}
#[test]
fn platform_expr_preflight_rejects_malformed_expression_with_field_name() {
let mut config = PlatformExprStrategyConfig::microcap_rotation();
config.exposure_expr = "safe_div(1.0, 2.0".to_string();
let error = PlatformExprStrategy::new(config)
.validate_expression_syntax()
.expect_err("malformed expression must fail before data loading");
let message = error.to_string();
assert!(message.contains("preflight compile failed"), "{message}");
assert!(message.contains("field=exposure_expr"), "{message}");
}
#[test] #[test]
fn platform_expr_safe_div_supports_two_and_three_arg_forms() { fn platform_expr_safe_div_supports_two_and_three_arg_forms() {
let strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); let strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation());