补齐回测卖出侧缺失风控事实拒绝

This commit is contained in:
boris
2026-07-02 11:29:38 +08:00
parent baa77c68e0
commit 8715a6171a
+109 -2
View File
@@ -381,7 +381,47 @@ impl ChinaAShareRiskControl {
|| config.static_rules.reject_one_yuan_buy || config.static_rules.reject_one_yuan_buy
|| config.static_rules.respect_allow_buy_sell || config.static_rules.respect_allow_buy_sell
} }
RiskCheckScope::Sell => false, 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"
)
}))
}
} }
} }
@@ -466,6 +506,9 @@ impl ChinaAShareRiskControl {
) { ) {
return Some(reason); 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) { if config.static_rules.reject_paused_sell && (market.paused || candidate.is_paused) {
return Some("paused"); return Some("paused");
} }
@@ -496,6 +539,18 @@ impl ChinaAShareRiskControl {
} }
} }
fn missing_risk_state_fields(code: &str) -> Vec<String> {
let Some((_, fields)) = code.split_once(':') else {
return Vec::new();
};
fields
.split(|ch| matches!(ch, ',' | ';' | '|' | ' '))
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|value| value.to_ascii_lowercase())
.collect()
}
#[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 {
@@ -644,7 +699,7 @@ mod tests {
fn missing_risk_state_rejects_selection_and_buy_when_static_filters_enabled() { fn missing_risk_state_rejects_selection_and_buy_when_static_filters_enabled() {
let date = d(2025, 1, 2); let date = d(2025, 1, 2);
let mut candidate = candidate(date); let mut candidate = candidate(date);
candidate.risk_level_code = Some("missing_risk_state:is_st,paused,allow_buy".to_string()); candidate.risk_level_code = Some("missing_risk_state:is_st,allow_buy".to_string());
let market = market(date, 6.27, 5.63); let market = market(date, 6.27, 5.63);
let selection_reason = let selection_reason =
@@ -665,6 +720,58 @@ mod tests {
assert_eq!(sell_reason, None); assert_eq!(sell_reason, None);
} }
#[test]
fn missing_risk_state_rejects_sell_when_sell_risk_facts_are_missing() {
let date = d(2025, 1, 2);
let mut candidate = candidate(date);
candidate.allow_sell = true;
candidate.risk_level_code = Some(
"missing_risk_state:paused,active_status,allow_sell,lower_limit_price".to_string(),
);
let market = market(date, 6.27, 5.63);
let position = position(d(2024, 12, 31));
let reason = ChinaAShareRiskControl::sell_rejection_reason(
date,
&candidate,
&market,
None,
Some(&position),
6.27,
);
assert_eq!(reason, Some("missing_risk_state"));
}
#[test]
fn missing_risk_state_sell_can_be_relaxed_by_disabling_sell_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:paused,active_status,allow_sell,lower_limit_price".to_string(),
);
let market = market(date, 6.27, 5.63);
let position = position(d(2024, 12, 31));
let mut config = FidcRiskControlConfig::default();
config.static_rules.reject_paused_sell = false;
config.static_rules.reject_inactive_sell = false;
config.static_rules.respect_allow_buy_sell = false;
config.static_rules.reject_lower_limit_sell = false;
let reason = ChinaAShareRiskControl::sell_rejection_reason_with_config(
date,
&candidate,
&market,
None,
Some(&position),
6.27,
&config,
);
assert_eq!(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);