From 50120e0f9b6817af9d035e8ce26d084a832029c0 Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 2 Jul 2026 11:40:08 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=86=E5=8C=96=E7=BC=BA=E5=A4=B1=E9=A3=8E?= =?UTF-8?q?=E6=8E=A7=E4=BA=8B=E5=AE=9E=E5=BC=80=E5=85=B3=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/risk_control.rs | 246 ++++++++++++++++++++------- 1 file changed, 187 insertions(+), 59 deletions(-) diff --git a/crates/fidc-core/src/risk_control.rs b/crates/fidc-core/src/risk_control.rs index 5b19151..8ab8990 100644 --- a/crates/fidc-core/src/risk_control.rs +++ b/crates/fidc-core/src/risk_control.rs @@ -363,65 +363,9 @@ impl ChinaAShareRiskControl { return false; } match scope { - RiskCheckScope::Selection => { - config.static_rules.reject_st_selection - || config.static_rules.reject_paused_selection - || config.static_rules.reject_inactive_selection - || config.static_rules.reject_new_listing_selection - || config.static_rules.reject_kcb_selection - || config.static_rules.reject_one_yuan_selection - || config.static_rules.respect_allow_buy_sell - } - RiskCheckScope::Buy => { - config.static_rules.reject_st_buy - || config.static_rules.reject_paused_buy - || config.static_rules.reject_inactive_buy - || config.static_rules.reject_new_listing_buy - || config.static_rules.reject_kcb_buy - || config.static_rules.reject_one_yuan_buy - || config.static_rules.respect_allow_buy_sell - } - 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" - ) - })) - } + RiskCheckScope::Selection => missing_selection_risk_state_rejected(code, config), + RiskCheckScope::Buy => missing_buy_risk_state_rejected(code, config), + RiskCheckScope::Sell => missing_sell_risk_state_rejected(code, config), } } @@ -551,6 +495,149 @@ fn missing_risk_state_fields(code: &str) -> Vec { .collect() } +fn missing_selection_risk_state_rejected(code: &str, config: &FidcRiskControlConfig) -> bool { + let fields = missing_risk_state_fields(code); + if fields.is_empty() { + return config.static_rules.reject_st_selection + || config.static_rules.reject_paused_selection + || config.static_rules.reject_inactive_selection + || config.static_rules.reject_new_listing_selection + || config.static_rules.reject_kcb_selection + || config.static_rules.reject_one_yuan_selection + || config.static_rules.reject_upper_limit_selection + || config.static_rules.reject_lower_limit_selection + || config.static_rules.respect_allow_buy_sell; + } + missing_field_rejected(&fields, config, RiskCheckScope::Selection) +} + +fn missing_buy_risk_state_rejected(code: &str, config: &FidcRiskControlConfig) -> bool { + let fields = missing_risk_state_fields(code); + if fields.is_empty() { + return config.static_rules.reject_st_buy + || config.static_rules.reject_paused_buy + || config.static_rules.reject_inactive_buy + || config.static_rules.reject_new_listing_buy + || config.static_rules.reject_kcb_buy + || config.static_rules.reject_one_yuan_buy + || config.static_rules.reject_upper_limit_buy + || config.static_rules.respect_allow_buy_sell; + } + missing_field_rejected(&fields, config, RiskCheckScope::Buy) +} + +fn missing_sell_risk_state_rejected(code: &str, config: &FidcRiskControlConfig) -> bool { + 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; + } + missing_field_rejected(&fields, config, RiskCheckScope::Sell) +} + +fn missing_field_rejected( + fields: &[String], + config: &FidcRiskControlConfig, + scope: RiskCheckScope, +) -> bool { + fields + .iter() + .any(|field| missing_single_field_rejected(field, config, scope)) +} + +fn missing_single_field_rejected( + field: &str, + config: &FidcRiskControlConfig, + scope: RiskCheckScope, +) -> bool { + match field { + "is_st" | "is_star_st" | "st" | "star_st" | "star_st_status" => match scope { + RiskCheckScope::Selection => config.static_rules.reject_st_selection, + RiskCheckScope::Buy => config.static_rules.reject_st_buy, + RiskCheckScope::Sell => false, + }, + "paused" | "is_paused" | "suspended" | "is_suspended" => match scope { + RiskCheckScope::Selection => config.static_rules.reject_paused_selection, + RiskCheckScope::Buy => config.static_rules.reject_paused_buy, + RiskCheckScope::Sell => config.static_rules.reject_paused_sell, + }, + "active_status" | "status" | "instrument_status" | "is_active" | "inactive_status" + | "is_inactive" | "is_delisted" | "listed_date" | "delisted_date" => 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, + }, + "listed_days" | "listing_days" | "days_since_listing" => match scope { + RiskCheckScope::Selection => config.static_rules.reject_new_listing_selection, + RiskCheckScope::Buy => config.static_rules.reject_new_listing_buy, + RiskCheckScope::Sell => false, + }, + "is_kcb" | "kcb" | "board" | "market_board" => match scope { + RiskCheckScope::Selection => config.static_rules.reject_kcb_selection, + RiskCheckScope::Buy => config.static_rules.reject_kcb_buy, + RiskCheckScope::Sell => false, + }, + "is_one_yuan" | "one_yuan" => match scope { + RiskCheckScope::Selection => config.static_rules.reject_one_yuan_selection, + RiskCheckScope::Buy => config.static_rules.reject_one_yuan_buy, + RiskCheckScope::Sell => false, + }, + "allow_buy" => match scope { + RiskCheckScope::Selection | RiskCheckScope::Buy => { + config.static_rules.respect_allow_buy_sell + } + RiskCheckScope::Sell => false, + }, + "allow_sell" => match scope { + RiskCheckScope::Selection | RiskCheckScope::Sell => { + config.static_rules.respect_allow_buy_sell + } + RiskCheckScope::Buy => false, + }, + "upper_limit" | "upper_limit_price" | "high_limit" | "high_limit_price" => match scope { + RiskCheckScope::Selection => config.static_rules.reject_upper_limit_selection, + RiskCheckScope::Buy => config.static_rules.reject_upper_limit_buy, + RiskCheckScope::Sell => false, + }, + "lower_limit" | "lower_limit_price" | "low_limit" | "low_limit_price" => match scope { + RiskCheckScope::Selection => config.static_rules.reject_lower_limit_selection, + RiskCheckScope::Buy => false, + RiskCheckScope::Sell => config.static_rules.reject_lower_limit_sell, + }, + _ => match scope { + RiskCheckScope::Selection => { + config.static_rules.reject_st_selection + || config.static_rules.reject_paused_selection + || config.static_rules.reject_inactive_selection + || config.static_rules.reject_new_listing_selection + || config.static_rules.reject_kcb_selection + || config.static_rules.reject_one_yuan_selection + || config.static_rules.reject_upper_limit_selection + || config.static_rules.reject_lower_limit_selection + || config.static_rules.respect_allow_buy_sell + } + RiskCheckScope::Buy => { + config.static_rules.reject_st_buy + || config.static_rules.reject_paused_buy + || config.static_rules.reject_inactive_buy + || config.static_rules.reject_new_listing_buy + || config.static_rules.reject_kcb_buy + || config.static_rules.reject_one_yuan_buy + || config.static_rules.reject_upper_limit_buy + || config.static_rules.respect_allow_buy_sell + } + RiskCheckScope::Sell => { + config.static_rules.reject_paused_sell + || config.static_rules.reject_inactive_sell + || config.static_rules.reject_lower_limit_sell + || config.static_rules.respect_allow_buy_sell + } + }, + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RiskCheckScope { @@ -772,6 +859,47 @@ mod tests { assert_eq!(reason, None); } + #[test] + fn missing_risk_state_respects_field_specific_selection_switches() { + let date = d(2025, 1, 2); + let mut candidate = candidate(date); + candidate.allow_sell = true; + candidate.risk_level_code = + Some("missing_risk_state:upper_limit_price,lower_limit_price".to_string()); + let market = market(date, 6.27, 5.63); + let mut config = FidcRiskControlConfig::default(); + config.static_rules.reject_upper_limit_selection = false; + + let upper_disabled_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config( + date, &candidate, &market, None, &config, + ); + + config.static_rules.reject_lower_limit_selection = false; + let both_disabled_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config( + date, &candidate, &market, None, &config, + ); + + assert_eq!(upper_disabled_reason, Some("missing_risk_state")); + assert_eq!(both_disabled_reason, None); + } + + #[test] + fn missing_risk_state_respects_field_specific_buy_switches() { + let date = d(2025, 1, 2); + let mut candidate = candidate(date); + candidate.risk_level_code = + Some("missing_risk_state:upper_limit_price,lower_limit_price".to_string()); + let market = market(date, 6.27, 5.63); + let mut config = FidcRiskControlConfig::default(); + config.static_rules.reject_upper_limit_buy = false; + + let buy_reason = ChinaAShareRiskControl::buy_rejection_reason_with_config( + date, &candidate, &market, None, 6.27, &config, + ); + + assert_eq!(buy_reason, None); + } + #[test] fn missing_risk_state_can_be_relaxed_by_disabling_static_filters() { let date = d(2025, 1, 2);