细化缺失风控事实开关判断
This commit is contained in:
@@ -363,65 +363,9 @@ impl ChinaAShareRiskControl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
match scope {
|
match scope {
|
||||||
RiskCheckScope::Selection => {
|
RiskCheckScope::Selection => missing_selection_risk_state_rejected(code, config),
|
||||||
config.static_rules.reject_st_selection
|
RiskCheckScope::Buy => missing_buy_risk_state_rejected(code, config),
|
||||||
|| config.static_rules.reject_paused_selection
|
RiskCheckScope::Sell => missing_sell_risk_state_rejected(code, config),
|
||||||
|| 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"
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,6 +495,149 @@ fn missing_risk_state_fields(code: &str) -> Vec<String> {
|
|||||||
.collect()
|
.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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum RiskCheckScope {
|
pub enum RiskCheckScope {
|
||||||
@@ -772,6 +859,47 @@ mod tests {
|
|||||||
assert_eq!(reason, None);
|
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]
|
#[test]
|
||||||
fn missing_risk_state_can_be_relaxed_by_disabling_static_filters() {
|
fn missing_risk_state_can_be_relaxed_by_disabling_static_filters() {
|
||||||
let date = d(2025, 1, 2);
|
let date = d(2025, 1, 2);
|
||||||
|
|||||||
Reference in New Issue
Block a user