From 995dd96117fa517c828ccbddcd98034245022d5b Mon Sep 17 00:00:00 2001 From: boris Date: Sat, 4 Jul 2026 05:56:42 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=9B=9E=E6=B5=8B=E9=A3=8E?= =?UTF-8?q?=E6=8E=A7=E9=85=8D=E7=BD=AE=E5=BD=92=E4=B8=80=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 38 +++++-- .../fidc-core/src/platform_strategy_spec.rs | 101 ++++++++++++++---- 2 files changed, 112 insertions(+), 27 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 4bb3e53..2d80519 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -5915,6 +5915,16 @@ impl PlatformExprStrategy { ctx: &StrategyContext<'_>, date: NaiveDate, factor_date: NaiveDate, + ) -> Vec { + self.selectable_universe_on_with_options(ctx, date, factor_date, false) + } + + fn selectable_universe_on_with_options( + &self, + ctx: &StrategyContext<'_>, + date: NaiveDate, + factor_date: NaiveDate, + defer_limit_state_selection_risk: bool, ) -> Vec { let mut rows = Vec::new(); for factor in ctx.data.factor_snapshots_on(factor_date) { @@ -5930,12 +5940,20 @@ impl PlatformExprStrategy { let Some(market) = ctx.data.market(date, &factor.symbol) else { continue; }; - if !ctx.is_lagged_execution() - && self - .selection_risk_rejection_reason(ctx, date, &factor.symbol, candidate, market) - .is_some() - { - continue; + if !ctx.is_lagged_execution() { + if let Some(reason) = self.selection_risk_rejection_reason( + ctx, + date, + &factor.symbol, + candidate, + market, + ) { + if !(defer_limit_state_selection_risk + && matches!(reason, "upper_limit" | "lower_limit")) + { + continue; + } + } } if !self.stock_passes_universe_exclude(candidate, market) { continue; @@ -7024,10 +7042,16 @@ impl PlatformExprStrategy { band_high: f64, selection_limit: usize, ) -> Result<(Vec, Vec, usize, Vec), BacktestError> { - let universe = self.selectable_universe_on(ctx, date, universe_factor_date); let mut diagnostics = Vec::new(); let mut candidates = Vec::new(); let quote_usage = self.stock_filter_quote_usage(); + let universe = self.selectable_universe_on_with_options( + ctx, + date, + universe_factor_date, + self.selection_uses_intraday_quote_fields() + || quote_usage != StockFilterQuoteUsage::DailyOnly, + ); let quote_candidate_limit = self.quote_plan_candidate_limit(selection_limit); for candidate in universe { let stock = self.selection_stock_state_with_factor_date( diff --git a/crates/fidc-core/src/platform_strategy_spec.rs b/crates/fidc-core/src/platform_strategy_spec.rs index 8b5fac9..5eec821 100644 --- a/crates/fidc-core/src/platform_strategy_spec.rs +++ b/crates/fidc-core/src/platform_strategy_spec.rs @@ -232,7 +232,13 @@ pub struct StrategyRiskPolicySpec { pub forbid_same_day_rebuy_after_sell: Option, #[serde(default, alias = "blacklist_enabled")] pub blacklist_enabled: Option, - #[serde(default, alias = "blacklisted_symbols", alias = "blacklist")] + #[serde( + default, + alias = "blacklisted_symbols", + alias = "blacklistedInstruments", + alias = "blacklisted_instruments", + alias = "blacklist" + )] pub blacklisted_symbols: Vec, #[serde(default, alias = "volume_limit_enabled")] pub volume_limit_enabled: Option, @@ -331,6 +337,40 @@ fn remove_policy_alias_values( values } +fn parse_policy_bool(canonical: &str, value: Value) -> Result { + match value { + Value::Bool(value) => Ok(value), + Value::Number(number) => match number + .as_i64() + .or_else(|| number.as_u64().map(|v| v as i64)) + { + Some(0) => Ok(false), + Some(1) => Ok(true), + _ => Err(format!("riskPolicy.{canonical} must be a boolean")), + }, + Value::String(raw) => match raw.trim().to_ascii_lowercase().as_str() { + "true" | "1" | "yes" | "y" | "on" => Ok(true), + "false" | "0" | "no" | "n" | "off" => Ok(false), + _ => Err(format!("riskPolicy.{canonical} must be a boolean")), + }, + _ => Err(format!("riskPolicy.{canonical} must be a boolean")), + } +} + +fn push_blacklist_symbols_from_str( + raw: &str, + symbols: &mut Vec, + seen: &mut HashSet, +) { + for item in raw.split([',', ';', '\n', '\r', '|']) { + let key = item.trim().to_string(); + if key.is_empty() || !seen.insert(key.clone()) { + continue; + } + symbols.push(Value::String(key)); + } +} + fn normalize_risk_policy_object_aliases( object: &mut serde_json::Map, ) -> Result<(), String> { @@ -339,10 +379,7 @@ fn normalize_risk_policy_object_aliases( if !values.is_empty() { let mut merged = false; for value in values { - let Some(value) = value.as_bool() else { - return Err(format!("riskPolicy.{canonical} must be a boolean")); - }; - merged |= value; + merged |= parse_policy_bool(canonical, value)?; } object.insert((*canonical).to_string(), Value::Bool(merged)); } @@ -358,7 +395,12 @@ fn normalize_risk_policy_object_aliases( let blacklist_values = remove_policy_alias_values( object, "blacklistedSymbols", - &["blacklisted_symbols", "blacklist"], + &[ + "blacklisted_symbols", + "blacklistedInstruments", + "blacklisted_instruments", + "blacklist", + ], ); let mut symbols = Vec::::new(); let mut seen = HashSet::::new(); @@ -371,18 +413,11 @@ fn normalize_risk_policy_object_aliases( "riskPolicy.blacklistedSymbols must contain only strings".to_string() ); }; - let key = symbol.trim().to_string(); - if key.is_empty() || !seen.insert(key.clone()) { - continue; - } - symbols.push(Value::String(key)); + push_blacklist_symbols_from_str(symbol, &mut symbols, &mut seen); } } Value::String(item) => { - let key = item.trim().to_string(); - if !key.is_empty() && seen.insert(key.clone()) { - symbols.push(Value::String(key)); - } + push_blacklist_symbols_from_str(&item, &mut symbols, &mut seen); } _ => { return Err( @@ -2242,16 +2277,18 @@ mod tests { let spec = serde_json::json!({ "engine_config": { "risk_policy": { - "reject_st_selection": false, - "rejectStarStSelection": true, - "reject_st_buy": false, - "rejectStarStBuy": true, - "reject_paused_selection": false, + "reject_st_selection": "off", + "rejectStarStSelection": "yes", + "reject_st_buy": 0, + "rejectStarStBuy": 1, + "reject_paused_selection": "false", "reject_kcb_buy": false, "rejectBjseSelection": false, "reject_bjse_buy": false, "blacklisted_symbols": ["000001.SZ"], "blacklist": ["000002.SZ", "000001.SZ"], + "blacklistedInstruments": "000003.SZ|000004.SZ", + "blacklisted_instruments": ["000005.SZ,000006.SZ"], "volume_limit_enabled": true, "volume_percent": 25, "min_commission": 6.0, @@ -2288,6 +2325,30 @@ mod tests { .blacklisted_symbols .contains("000002.SZ") ); + assert!( + cfg.risk_config + .static_rules + .blacklisted_symbols + .contains("000003.SZ") + ); + assert!( + cfg.risk_config + .static_rules + .blacklisted_symbols + .contains("000004.SZ") + ); + assert!( + cfg.risk_config + .static_rules + .blacklisted_symbols + .contains("000005.SZ") + ); + assert!( + cfg.risk_config + .static_rules + .blacklisted_symbols + .contains("000006.SZ") + ); assert!((cfg.risk_config.trading_constraints.volume_percent - 0.25).abs() < 1e-12); assert_eq!(cfg.risk_config.trading_constraints.minimum_commission, 6.0); assert_eq!(cfg.commission_rate, Some(0.0003));