细化缺失风控事实开关判断

This commit is contained in:
boris
2026-07-02 11:40:08 +08:00
parent 8715a6171a
commit 50120e0f9b
+187 -59
View File
@@ -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<String> {
.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);