From 8715a6171a8b152cbe3c101e5885925a82d2193a Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 2 Jul 2026 11:29:38 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E5=9B=9E=E6=B5=8B=E5=8D=96?= =?UTF-8?q?=E5=87=BA=E4=BE=A7=E7=BC=BA=E5=A4=B1=E9=A3=8E=E6=8E=A7=E4=BA=8B?= =?UTF-8?q?=E5=AE=9E=E6=8B=92=E7=BB=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/risk_control.rs | 111 ++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/crates/fidc-core/src/risk_control.rs b/crates/fidc-core/src/risk_control.rs index 825fb59..5b19151 100644 --- a/crates/fidc-core/src/risk_control.rs +++ b/crates/fidc-core/src/risk_control.rs @@ -381,7 +381,47 @@ impl ChinaAShareRiskControl { || config.static_rules.reject_one_yuan_buy || config.static_rules.respect_allow_buy_sell } - RiskCheckScope::Sell => false, + RiskCheckScope::Sell => { + let fields = missing_risk_state_fields(code); + if fields.is_empty() { + return config.static_rules.reject_paused_sell + || config.static_rules.reject_inactive_sell + || config.static_rules.respect_allow_buy_sell + || config.static_rules.reject_lower_limit_sell; + } + (config.static_rules.reject_paused_sell + && fields.iter().any(|field| { + matches!( + field.as_str(), + "paused" | "is_paused" | "suspended" | "is_suspended" + ) + })) + || (config.static_rules.reject_inactive_sell + && fields.iter().any(|field| { + matches!( + field.as_str(), + "active_status" + | "is_active" + | "inactive_status" + | "is_inactive" + | "is_delisted" + | "listed_date" + | "delisted_date" + ) + })) + || (config.static_rules.respect_allow_buy_sell + && fields.iter().any(|field| field == "allow_sell")) + || (config.static_rules.reject_lower_limit_sell + && fields.iter().any(|field| { + matches!( + field.as_str(), + "lower_limit" + | "lower_limit_price" + | "low_limit" + | "low_limit_price" + ) + })) + } } } @@ -466,6 +506,9 @@ impl ChinaAShareRiskControl { ) { return Some(reason); } + if Self::missing_risk_state_rejected(candidate, config, RiskCheckScope::Sell) { + return Some("missing_risk_state"); + } if config.static_rules.reject_paused_sell && (market.paused || candidate.is_paused) { return Some("paused"); } @@ -496,6 +539,18 @@ impl ChinaAShareRiskControl { } } +fn missing_risk_state_fields(code: &str) -> Vec { + let Some((_, fields)) = code.split_once(':') else { + return Vec::new(); + }; + fields + .split(|ch| matches!(ch, ',' | ';' | '|' | ' ')) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(|value| value.to_ascii_lowercase()) + .collect() +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RiskCheckScope { @@ -644,7 +699,7 @@ mod tests { fn missing_risk_state_rejects_selection_and_buy_when_static_filters_enabled() { let date = d(2025, 1, 2); let mut candidate = candidate(date); - candidate.risk_level_code = Some("missing_risk_state:is_st,paused,allow_buy".to_string()); + candidate.risk_level_code = Some("missing_risk_state:is_st,allow_buy".to_string()); let market = market(date, 6.27, 5.63); let selection_reason = @@ -665,6 +720,58 @@ mod tests { assert_eq!(sell_reason, None); } + #[test] + fn missing_risk_state_rejects_sell_when_sell_risk_facts_are_missing() { + let date = d(2025, 1, 2); + let mut candidate = candidate(date); + candidate.allow_sell = true; + candidate.risk_level_code = Some( + "missing_risk_state:paused,active_status,allow_sell,lower_limit_price".to_string(), + ); + let market = market(date, 6.27, 5.63); + let position = position(d(2024, 12, 31)); + + let reason = ChinaAShareRiskControl::sell_rejection_reason( + date, + &candidate, + &market, + None, + Some(&position), + 6.27, + ); + + assert_eq!(reason, Some("missing_risk_state")); + } + + #[test] + fn missing_risk_state_sell_can_be_relaxed_by_disabling_sell_static_filters() { + let date = d(2025, 1, 2); + let mut candidate = candidate(date); + candidate.allow_sell = true; + candidate.risk_level_code = Some( + "missing_risk_state:paused,active_status,allow_sell,lower_limit_price".to_string(), + ); + let market = market(date, 6.27, 5.63); + let position = position(d(2024, 12, 31)); + let mut config = FidcRiskControlConfig::default(); + config.static_rules.reject_paused_sell = false; + config.static_rules.reject_inactive_sell = false; + config.static_rules.respect_allow_buy_sell = false; + config.static_rules.reject_lower_limit_sell = false; + + let reason = ChinaAShareRiskControl::sell_rejection_reason_with_config( + date, + &candidate, + &market, + None, + Some(&position), + 6.27, + &config, + ); + + assert_eq!(reason, None); + } + #[test] fn missing_risk_state_can_be_relaxed_by_disabling_static_filters() { let date = d(2025, 1, 2);