From b15b93eec11e7726300c8d5040a9ba427a843b06 Mon Sep 17 00:00:00 2001 From: boris Date: Wed, 2 Sep 2026 19:05:28 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=9B=AE=E6=A0=87=E6=9D=83?= =?UTF-8?q?=E9=87=8D=E6=98=A0=E5=B0=84=E9=A2=84=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 76 +++++++++++++++++-- 1 file changed, 71 insertions(+), 5 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 98dd89c..4f01139 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -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 { + 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)