From baa77c68e04bef5d3bc6433573aa3e17433c05d5 Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 2 Jul 2026 09:41:07 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E7=BC=BA=E5=A4=B1=E9=A3=8E?= =?UTF-8?q?=E6=8E=A7=E7=8A=B6=E6=80=81=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 | 100 +++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/crates/fidc-core/src/risk_control.rs b/crates/fidc-core/src/risk_control.rs index 235af67..825fb59 100644 --- a/crates/fidc-core/src/risk_control.rs +++ b/crates/fidc-core/src/risk_control.rs @@ -300,6 +300,9 @@ impl ChinaAShareRiskControl { { return Some(reason); } + if Self::missing_risk_state_rejected(candidate, config, scope) { + return Some("missing_risk_state"); + } let reject_paused = match scope { RiskCheckScope::Selection => config.static_rules.reject_paused_selection, RiskCheckScope::Buy => config.static_rules.reject_paused_buy, @@ -343,6 +346,45 @@ impl ChinaAShareRiskControl { None } + fn missing_risk_state_rejected( + candidate: &CandidateEligibility, + config: &FidcRiskControlConfig, + scope: RiskCheckScope, + ) -> bool { + let Some(code) = candidate + .risk_level_code + .as_deref() + .map(str::trim) + .filter(|value| value.starts_with("missing_risk_state")) + else { + return false; + }; + if code.is_empty() { + 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 => false, + } + } + pub fn buy_rejection_reason( date: NaiveDate, candidate: &CandidateEligibility, @@ -598,6 +640,64 @@ mod tests { assert_eq!(configured_reason, None); } + #[test] + 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()); + let market = market(date, 6.27, 5.63); + + let selection_reason = + ChinaAShareRiskControl::selection_rejection_reason(date, &candidate, &market, None); + let buy_reason = + ChinaAShareRiskControl::buy_rejection_reason(date, &candidate, &market, None, 6.27); + let sell_reason = ChinaAShareRiskControl::sell_rejection_reason( + date, + &candidate, + &market, + None, + Some(&position(d(2024, 12, 31))), + 6.27, + ); + + assert_eq!(selection_reason, Some("missing_risk_state")); + assert_eq!(buy_reason, Some("missing_risk_state")); + assert_eq!(sell_reason, None); + } + + #[test] + fn missing_risk_state_can_be_relaxed_by_disabling_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:is_st,paused,allow_buy".to_string()); + let market = market(date, 6.27, 5.63); + let mut config = FidcRiskControlConfig::default(); + config.static_rules.reject_st_selection = false; + config.static_rules.reject_st_buy = false; + config.static_rules.reject_paused_selection = false; + config.static_rules.reject_paused_buy = false; + config.static_rules.reject_inactive_selection = false; + config.static_rules.reject_inactive_buy = false; + config.static_rules.reject_new_listing_selection = false; + config.static_rules.reject_new_listing_buy = false; + config.static_rules.reject_kcb_selection = false; + config.static_rules.reject_kcb_buy = false; + config.static_rules.reject_one_yuan_selection = false; + config.static_rules.reject_one_yuan_buy = false; + config.static_rules.respect_allow_buy_sell = false; + + let selection_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config( + date, &candidate, &market, None, &config, + ); + let buy_reason = ChinaAShareRiskControl::buy_rejection_reason_with_config( + date, &candidate, &market, None, 6.27, &config, + ); + + assert_eq!(selection_reason, None); + assert_eq!(buy_reason, None); + } + #[test] fn configurable_upper_limit_buy_filter_can_be_disabled() { let date = d(2025, 1, 2);