修复目标权重映射预校验
This commit is contained in:
@@ -1397,8 +1397,36 @@ impl PlatformExprStrategy {
|
||||
/// 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.
|
||||
fn collect_preflight_float_map_values(
|
||||
field: &str,
|
||||
expression: &str,
|
||||
output: &mut Vec<(String, String)>,
|
||||
) -> Result<bool, BacktestError> {
|
||||
let trimmed = expression.trim();
|
||||
let Some(inner) = trimmed
|
||||
.strip_prefix('{')
|
||||
.and_then(|value| value.strip_suffix('}'))
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
for (index, entry) in Self::split_top_level_args(inner).into_iter().enumerate() {
|
||||
let Some((raw_key, raw_value)) = Self::split_top_level_key_value(&entry) else {
|
||||
return Err(BacktestError::Execution(format!(
|
||||
"platform float map entry must be key: value, got {entry}"
|
||||
)));
|
||||
};
|
||||
let key = Self::parse_string_literal_key(raw_key)?;
|
||||
output.push((
|
||||
format!("{field}[{index}:{key}]"),
|
||||
raw_value.trim().to_string(),
|
||||
));
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn validate_expression_syntax(&self) -> Result<(), BacktestError> {
|
||||
let normalized_prelude = Self::normalize_prelude_for_eval(&self.config.prelude);
|
||||
let mut float_map_values = Vec::<(String, String)>::new();
|
||||
let mut expressions = vec![
|
||||
(
|
||||
"refresh_rate_expr".to_string(),
|
||||
@@ -1475,18 +1503,32 @@ impl PlatformExprStrategy {
|
||||
when_expr,
|
||||
..
|
||||
} => {
|
||||
expressions.push((
|
||||
format!("explicit_actions[{index}].target_weights_expr"),
|
||||
let target_field =
|
||||
format!("explicit_actions[{index}].target_weights_expr");
|
||||
if !Self::collect_preflight_float_map_values(
|
||||
&target_field,
|
||||
target_weights_expr,
|
||||
));
|
||||
&mut float_map_values,
|
||||
)? {
|
||||
expressions.push((target_field, 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));
|
||||
let field = format!("explicit_actions[{index}].{name}");
|
||||
if name != "when_expr"
|
||||
&& Self::collect_preflight_float_map_values(
|
||||
&field,
|
||||
expression,
|
||||
&mut float_map_values,
|
||||
)?
|
||||
{
|
||||
continue;
|
||||
}
|
||||
expressions.push((field, expression));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1612,6 +1654,22 @@ impl PlatformExprStrategy {
|
||||
))
|
||||
})?;
|
||||
}
|
||||
for (field, expression) in float_map_values {
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -12906,6 +12964,14 @@ mod tests {
|
||||
when_expr: Some("true".to_string()),
|
||||
reason: "preflight".to_string(),
|
||||
},
|
||||
PlatformTradeAction::TargetPortfolioSmart {
|
||||
target_weights_expr: "{\"000001.SZ\": 0.6, \"000002.SZ\": 0.4}".to_string(),
|
||||
order_prices_expr: Some("{\"000001.SZ\": close * 1.01}".to_string()),
|
||||
valuation_prices_expr: Some("{\"000001.SZ\": close}".to_string()),
|
||||
time_in_force: None,
|
||||
when_expr: Some("cash > 0.0".to_string()),
|
||||
reason: "preflight_map".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
PlatformExprStrategy::new(config)
|
||||
|
||||
Reference in New Issue
Block a user