修复风控缺字段审计优先级

This commit is contained in:
boris
2026-07-02 21:39:04 +08:00
parent daa0a9b4e6
commit 25cc643f34
+85 -9
View File
@@ -122,6 +122,17 @@ impl FidcRiskDecisionAudit {
symbol: impl Into<String>,
reason: impl Into<String>,
) -> Self {
let reason = reason.into();
Self::rejected_selection_with_rule(date, symbol, reason.clone(), reason)
}
pub fn rejected_selection_with_rule(
date: NaiveDate,
symbol: impl Into<String>,
rule_code: impl Into<String>,
reason: impl Into<String>,
) -> Self {
let rule_code = rule_code.into();
let reason = reason.into();
Self {
date,
@@ -129,7 +140,7 @@ impl FidcRiskDecisionAudit {
scope: RiskCheckScope::Selection,
stage: "selection".to_string(),
accepted: false,
rule_code: reason.clone(),
rule_code,
reason,
config_version: Some("inline_risk_policy".to_string()),
data_epoch: date.to_string(),
@@ -264,7 +275,22 @@ impl ChinaAShareRiskControl {
) -> Option<FidcRiskDecisionAudit> {
Self::selection_rejection_reason_with_config(date, candidate, market, instrument, config)
.map(|reason| {
FidcRiskDecisionAudit::rejected_selection(date, candidate.symbol.clone(), reason)
let detail = if reason == "missing_risk_state" {
candidate
.risk_level_code
.as_deref()
.map(str::trim)
.filter(|value| value.starts_with("missing_risk_state"))
.unwrap_or(reason)
} else {
reason
};
FidcRiskDecisionAudit::rejected_selection_with_rule(
date,
candidate.symbol.clone(),
reason,
detail,
)
})
}
@@ -300,9 +326,6 @@ 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,
@@ -332,7 +355,7 @@ impl ChinaAShareRiskControl {
RiskCheckScope::Buy => config.static_rules.reject_kcb_buy,
RiskCheckScope::Sell => false,
};
if reject_kcb && candidate.is_kcb {
if reject_kcb && (candidate.is_kcb || symbol_is_kcb(&candidate.symbol)) {
return Some("kcb");
}
let reject_one_yuan = match scope {
@@ -343,6 +366,9 @@ impl ChinaAShareRiskControl {
if reject_one_yuan && (candidate.is_one_yuan || market.day_open <= 1.0) {
return Some("one_yuan");
}
if Self::missing_risk_state_rejected(candidate, config, scope) {
return Some("missing_risk_state");
}
None
}
@@ -450,9 +476,6 @@ 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");
}
@@ -465,6 +488,9 @@ impl ChinaAShareRiskControl {
{
return Some("open at or below lower limit");
}
if Self::missing_risk_state_rejected(candidate, config, RiskCheckScope::Sell) {
return Some("missing_risk_state");
}
if position.is_some_and(|position| position.sellable_qty(date) == 0) {
return Some("t+1 sellable quantity is zero");
}
@@ -483,6 +509,11 @@ impl ChinaAShareRiskControl {
}
}
fn symbol_is_kcb(symbol: &str) -> bool {
let normalized = symbol.trim().to_ascii_uppercase();
(normalized.starts_with("688") || normalized.starts_with("689")) && normalized.ends_with(".SH")
}
fn missing_risk_state_fields(code: &str) -> Vec<String> {
let Some((_, fields)) = code.split_once(':') else {
return Vec::new();
@@ -782,6 +813,27 @@ mod tests {
assert_eq!(configured_reason, None);
}
#[test]
fn concrete_kcb_reason_wins_over_generic_missing_risk_state() {
let date = d(2025, 1, 2);
let mut candidate = candidate(date);
candidate.symbol = "688506.SH".to_string();
candidate.risk_level_code = Some("missing_risk_state".to_string());
let market = market(date, 6.27, 5.63);
let decision = ChinaAShareRiskControl::selection_rejection_decision_with_config(
date,
&candidate,
&market,
None,
&FidcRiskControlConfig::default(),
)
.expect("kcb selection rejection");
assert_eq!(decision.rule_code, "kcb");
assert_eq!(decision.reason, "kcb");
}
#[test]
fn missing_risk_state_rejects_selection_and_buy_when_static_filters_enabled() {
let date = d(2025, 1, 2);
@@ -807,6 +859,30 @@ mod tests {
assert_eq!(sell_reason, None);
}
#[test]
fn missing_risk_state_selection_audit_keeps_missing_fields_in_reason() {
let date = d(2025, 1, 2);
let mut candidate = candidate(date);
candidate.risk_level_code =
Some("missing_risk_state:is_st,allow_buy,upper_limit_price".to_string());
let market = market(date, 6.27, 5.63);
let decision = ChinaAShareRiskControl::selection_rejection_decision_with_config(
date,
&candidate,
&market,
None,
&FidcRiskControlConfig::default(),
)
.expect("missing risk state rejection");
assert_eq!(decision.rule_code, "missing_risk_state");
assert_eq!(
decision.reason,
"missing_risk_state:is_st,allow_buy,upper_limit_price"
);
}
#[test]
fn missing_risk_state_rejects_sell_when_sell_risk_facts_are_missing() {
let date = d(2025, 1, 2);