前置校验策略表达式语法
This commit is contained in:
@@ -1230,6 +1230,180 @@ impl PlatformExprStrategy {
|
||||
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;未命中:先
|
||||
/// engine.compile,再插入缓存,再 eval_ast_with_scope。任何编译/执行错误
|
||||
/// 都按字符串包装为 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]
|
||||
fn platform_expr_safe_div_supports_two_and_three_arg_forms() {
|
||||
let strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation());
|
||||
|
||||
Reference in New Issue
Block a user