前置校验策略表达式语法
This commit is contained in:
@@ -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());
|
||||||
|
|||||||
Reference in New Issue
Block a user