From b37ebb81f19e040f8d19a370fc90b3ba3a160a2b Mon Sep 17 00:00:00 2001 From: boris Date: Tue, 7 Jul 2026 07:19:10 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=AD=96=E7=95=A5=E6=98=BE?= =?UTF-8?q?=E5=BC=8F=E6=8E=92=E9=99=A4=E8=82=A1=E7=A5=A8=E5=8C=B9=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 66 +++++++++++++++---- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index b66d56b..eb956dc 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -6420,11 +6420,28 @@ impl PlatformExprStrategy { _candidate: &crate::data::CandidateEligibility, market: &DailyMarketSnapshot, ) -> bool { - let excludes = &self.config.universe_exclude; - if excludes.iter().any(|item| item == "bjse") && market.symbol.ends_with(".BJ") { - return false; + Self::universe_exclude_reason(&self.config.universe_exclude, &market.symbol).is_none() + } + + fn universe_exclude_reason(excludes: &[String], symbol: &str) -> Option<&'static str> { + let normalized_symbol = symbol.trim().to_ascii_lowercase(); + if normalized_symbol.is_empty() { + return None; } - true + let symbol_base = normalized_symbol.split('.').next().unwrap_or(""); + for raw_item in excludes { + let item = raw_item.trim().to_ascii_lowercase(); + if item.is_empty() { + continue; + } + if item == "bjse" && normalized_symbol.ends_with(".bj") { + return Some("bjse"); + } + if item == normalized_symbol || (!symbol_base.is_empty() && item == symbol_base) { + return Some("universe_exclude"); + } + } + None } fn stock_numeric_field_value( @@ -6652,14 +6669,10 @@ impl PlatformExprStrategy { let market = ctx.data.require_market(date, symbol)?; let candidate = ctx.data.require_candidate(date, symbol)?; - if self - .config - .universe_exclude - .iter() - .any(|item| item == "bjse") - && market.symbol.ends_with(".BJ") + if let Some(reason) = + Self::universe_exclude_reason(&self.config.universe_exclude, &market.symbol) { - return Ok(Some("bjse".to_string())); + return Ok(Some(reason.to_string())); } let upper_limit_check_price = self.projected_risk_check_price(ctx, date, symbol, &market, OrderSide::Buy, None); @@ -9093,6 +9106,37 @@ mod tests { NaiveDate::from_ymd_opt(year, month, day).expect("valid date") } + #[test] + fn universe_exclude_matches_explicit_symbols_and_bjse_alias() { + let excludes = vec![ + "920149.bj".to_string(), + "688555.sh".to_string(), + "000001".to_string(), + "bjse".to_string(), + ]; + + assert_eq!( + PlatformExprStrategy::universe_exclude_reason(&excludes, "920149.BJ"), + Some("universe_exclude") + ); + assert_eq!( + PlatformExprStrategy::universe_exclude_reason(&excludes, "688555.SH"), + Some("universe_exclude") + ); + assert_eq!( + PlatformExprStrategy::universe_exclude_reason(&excludes, "000001.SZ"), + Some("universe_exclude") + ); + assert_eq!( + PlatformExprStrategy::universe_exclude_reason(&["bjse".to_string()], "920508.BJ"), + Some("bjse") + ); + assert_eq!( + PlatformExprStrategy::universe_exclude_reason(&excludes, "300001.SZ"), + None + ); + } + #[test] fn platform_expr_rewrites_nested_ternary() { let expr = "pct_change(\"close\", 10) + (((close / rolling_mean(\"close\", 20)) - 1) > 0 ? 0.04 : -0.04)";