From e70d637ade405b8b01de0aff93d89a1106431eae Mon Sep 17 00:00:00 2001 From: boris Date: Sat, 4 Jul 2026 16:50:40 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3FIDC=E9=80=80=E5=B8=82?= =?UTF-8?q?=E9=A3=8E=E6=8E=A7=E5=8E=9F=E5=9B=A0=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/risk_control.rs | 56 ++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/crates/fidc-core/src/risk_control.rs b/crates/fidc-core/src/risk_control.rs index 353e801..0668be3 100644 --- a/crates/fidc-core/src/risk_control.rs +++ b/crates/fidc-core/src/risk_control.rs @@ -334,6 +334,9 @@ impl ChinaAShareRiskControl { { return Some(reason); } + if let Some(reason) = candidate_active_status_rejection(candidate, config, scope) { + return Some(reason); + } let reject_paused = match scope { RiskCheckScope::Selection => config.static_rules.reject_paused_selection, RiskCheckScope::Buy => config.static_rules.reject_paused_buy, @@ -543,6 +546,26 @@ fn symbol_is_bjse(symbol: &str) -> bool { normalized.ends_with(".BJ") || normalized.ends_with(".BSE") || normalized.ends_with(".BE") } +fn candidate_active_status_rejection( + candidate: &CandidateEligibility, + config: &FidcRiskControlConfig, + scope: RiskCheckScope, +) -> Option<&'static str> { + let enabled = match scope { + RiskCheckScope::Selection => config.static_rules.reject_inactive_selection, + RiskCheckScope::Buy => config.static_rules.reject_inactive_buy, + RiskCheckScope::Sell => config.static_rules.reject_inactive_sell, + }; + if !enabled { + return None; + } + match candidate.risk_level_code.as_deref().map(str::trim) { + Some("not_listed") => Some("not_listed"), + Some("inactive_or_delisted") => Some("inactive_or_delisted"), + _ => None, + } +} + fn missing_risk_state_fields(code: &str) -> Vec { let Some((_, fields)) = code.split_once(':') else { return Vec::new(); @@ -1007,6 +1030,39 @@ mod tests { assert_eq!(decision.reason, "kcb"); } + #[test] + fn explicit_candidate_inactive_status_is_not_treated_as_missing_risk_state() { + let date = d(2025, 1, 2); + let mut candidate = candidate(date); + candidate.risk_level_code = Some("inactive_or_delisted".to_string()); + let market = market(date, 6.27, 5.63); + + assert_eq!( + ChinaAShareRiskControl::selection_rejection_reason(date, &candidate, &market, None), + Some("inactive_or_delisted") + ); + assert_eq!( + ChinaAShareRiskControl::buy_rejection_reason(date, &candidate, &market, None, 6.27), + Some("inactive_or_delisted") + ); + + let mut relaxed = FidcRiskControlConfig::default(); + relaxed.static_rules.reject_inactive_selection = false; + relaxed.static_rules.reject_inactive_buy = false; + assert_eq!( + ChinaAShareRiskControl::selection_rejection_reason_with_config( + date, &candidate, &market, None, &relaxed + ), + None + ); + assert_eq!( + ChinaAShareRiskControl::buy_rejection_reason_with_config( + date, &candidate, &market, None, 6.27, &relaxed + ), + None + ); + } + #[test] fn missing_risk_state_rejects_selection_and_buy_when_static_filters_enabled() { let date = d(2025, 1, 2);