From 61bd14d0013e11bf46f4080ea0bf48bae3fb96ce Mon Sep 17 00:00:00 2001 From: boris Date: Sat, 12 Sep 2026 16:55:16 +0800 Subject: [PATCH] fix(backtest): inspect the authoritative strategy inside runner bundles --- crates/fidc-core/src/pattern_context.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/fidc-core/src/pattern_context.rs b/crates/fidc-core/src/pattern_context.rs index 558e4c1..1073c7c 100644 --- a/crates/fidc-core/src/pattern_context.rs +++ b/crates/fidc-core/src/pattern_context.rs @@ -212,6 +212,11 @@ pub fn build_dataset_context( } pub fn specs_in_value(value: &Value) -> Result, String> { + // A runner bundle also contains source/extract copies. Follow the same + // authoritative spec selection as the execution loader, not those copies. + if let Some(spec) = value.get("strategySpec").or_else(|| value.get("strategy_spec")) { + return specs_in_value(spec); + } let mut specs = Vec::new(); match value { Value::String(text) => specs.extend(crate::daily_patterns::expression_specs(text)?), @@ -268,9 +273,15 @@ mod tests { for (pool_key, source_key) in [("stockPool", "sourceCode"), ("stock_pool", "source_code")] { let value = json!({pool_key:pool,source_key:source,"runtimeExpressions":{"trading":{"buyFilterExpr":expr}}}); assert_eq!(specs_in_value(&value).unwrap().len(), 2); + for wrapper in ["strategySpec", "strategy_spec"] { + let bundle = json!({wrapper:value,"strategy_source":{"source_code":source}, + "strategy_extract":{"parameters":{"source_code":source}}}); + assert_eq!(specs_in_value(&bundle).unwrap().len(), 2); + } let mut invalid = value.clone(); invalid[pool_key]["exit_signals"][0]["when_expr"] = json!("pattern_signal(not-json)"); assert!(specs_in_value(&invalid).is_err(), "invalid actual conditions must still fail"); + assert!(specs_in_value(&json!({"strategySpec":invalid})).is_err()); } assert_eq!(specs_in_value(&json!({"sourceCode":format!("risk.stop_loss({expr})")})).unwrap().len(),1); }