补齐回测卖出侧缺失风控事实拒绝
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user