perf(risk): avoid per-symbol selection checks when the frozen policy has none

This commit is contained in:
boris
2026-09-14 02:18:22 +08:00
committed by boris
parent 0576cf9b6d
commit d2aa16a2f0
2 changed files with 89 additions and 23 deletions
@@ -10353,6 +10353,7 @@ impl PlatformExprStrategy {
) -> (Vec<u32>, Vec<FidcRiskDecisionAudit>) { ) -> (Vec<u32>, Vec<FidcRiskDecisionAudit>) {
let mut symbol_ids = Vec::new(); let mut symbol_ids = Vec::new();
let mut decisions = Vec::new(); let mut decisions = Vec::new();
let selection_checks_enabled = self.config.risk_config.static_rules.selection_checks_enabled();
let mut eligible_symbols = vec![false; ctx.data.symbol_count()]; let mut eligible_symbols = vec![false; ctx.data.symbol_count()];
let execution_day = ctx.data.daily_snapshot_view(date); let execution_day = ctx.data.daily_snapshot_view(date);
let factor_day = ctx.data.daily_snapshot_view(factor_date); let factor_day = ctx.data.daily_snapshot_view(factor_date);
@@ -10398,7 +10399,9 @@ impl PlatformExprStrategy {
let Some(market) = execution_day.market(symbol_id) else { let Some(market) = execution_day.market(symbol_id) else {
continue; continue;
}; };
let (reject_from_universe, selection_decision) = if collect_risk_decisions { let (reject_from_universe, selection_decision) = if !selection_checks_enabled {
(false, None)
} else if collect_risk_decisions {
let decision = ChinaAShareRiskControl::selection_rejection_decision_with_config( let decision = ChinaAShareRiskControl::selection_rejection_decision_with_config(
date, date,
candidate, candidate,
+85 -22
View File
@@ -76,6 +76,26 @@ impl Default for StaticRiskRuleConfig {
} }
} }
impl StaticRiskRuleConfig {
pub(crate) fn selection_checks_enabled(&self) -> bool {
(self.blacklist_enabled && !self.blacklisted_symbols.is_empty())
|| self.selection_state_checks_enabled()
}
fn selection_state_checks_enabled(&self) -> bool {
self.reject_st_selection
|| self.reject_star_st_selection
|| self.reject_paused_selection
|| self.reject_inactive_selection
|| self.reject_new_listing_selection
|| self.reject_kcb_selection
|| self.reject_bjse_selection
|| self.reject_one_yuan_selection
|| self.reject_upper_limit_selection
|| self.reject_lower_limit_selection
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct TradingConstraintConfig { pub struct TradingConstraintConfig {
/// Shared execution limits. These fields intentionally use the same /// Shared execution limits. These fields intentionally use the same
@@ -654,16 +674,7 @@ fn missing_risk_state_fields(code: &str) -> Vec<String> {
fn missing_selection_risk_state_rejected(code: &str, config: &FidcRiskControlConfig) -> bool { fn missing_selection_risk_state_rejected(code: &str, config: &FidcRiskControlConfig) -> bool {
let fields = missing_risk_state_fields(code); let fields = missing_risk_state_fields(code);
if fields.is_empty() { if fields.is_empty() {
return config.static_rules.reject_st_selection return config.static_rules.selection_state_checks_enabled();
|| config.static_rules.reject_star_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_bjse_selection
|| config.static_rules.reject_one_yuan_selection
|| config.static_rules.reject_upper_limit_selection
|| config.static_rules.reject_lower_limit_selection;
} }
missing_field_rejected(&fields, config, RiskCheckScope::Selection) missing_field_rejected(&fields, config, RiskCheckScope::Selection)
} }
@@ -778,18 +789,7 @@ fn missing_single_field_rejected(
RiskCheckScope::Sell => config.static_rules.reject_lower_limit_sell, RiskCheckScope::Sell => config.static_rules.reject_lower_limit_sell,
}, },
_ => match scope { _ => match scope {
RiskCheckScope::Selection => { RiskCheckScope::Selection => config.static_rules.selection_state_checks_enabled(),
config.static_rules.reject_st_selection
|| config.static_rules.reject_star_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_bjse_selection
|| config.static_rules.reject_one_yuan_selection
|| config.static_rules.reject_upper_limit_selection
|| config.static_rules.reject_lower_limit_selection
}
RiskCheckScope::Buy => { RiskCheckScope::Buy => {
config.static_rules.reject_st_buy config.static_rules.reject_st_buy
|| config.static_rules.reject_star_st_buy || config.static_rules.reject_star_st_buy
@@ -914,6 +914,69 @@ mod tests {
position position
} }
#[test]
fn selection_check_activation_covers_every_configured_flag_and_blacklist_state() {
let fields = [
"reject_st_selection", "reject_star_st_selection", "reject_paused_selection",
"reject_inactive_selection", "reject_new_listing_selection", "reject_kcb_selection",
"reject_bjse_selection", "reject_one_yuan_selection", "reject_upper_limit_selection",
"reject_lower_limit_selection",
];
let base = serde_json::to_value(StaticRiskRuleConfig::default()).unwrap();
let declared = base.as_object().unwrap().keys()
.filter(|key| key.ends_with("_selection"))
.map(String::as_str).collect::<BTreeSet<_>>();
assert_eq!(declared, fields.into_iter().collect());
for mask in 0..(1_u32 << fields.len()) {
for (blacklist_enabled, populated) in [(false, false), (false, true), (true, false), (true, true)] {
let mut value = base.clone();
for (bit, field) in fields.iter().enumerate() {
value[*field] = serde_json::json!(mask & (1 << bit) != 0);
}
value["blacklist_enabled"] = serde_json::json!(blacklist_enabled);
value["blacklisted_symbols"] = if populated {
serde_json::json!(["002633.SZ"])
} else { serde_json::json!([]) };
let config: StaticRiskRuleConfig = serde_json::from_value(value).unwrap();
assert_eq!(config.selection_checks_enabled(), mask != 0 || (blacklist_enabled && populated));
}
}
}
#[test]
fn inactive_selection_checks_preserve_missing_facts_and_execution_rejections() {
let date = d(2025, 2, 6);
let mut candidate = candidate(date);
candidate.is_st = true;
candidate.is_star_st = true;
candidate.is_paused = true;
candidate.is_new_listing = true;
candidate.is_kcb = true;
candidate.is_one_yuan = true;
candidate.allow_buy = false;
let snapshot = market(date, 0.9, 0.9);
let config = FidcRiskControlConfig::default();
assert!(!config.static_rules.selection_checks_enabled());
let instrument = instrument("delisted", Some(date));
for code in [None, Some("not_listed"), Some("inactive_or_delisted"),
Some("missing_risk_state"), Some("missing_risk_state:is_st;is_kcb|allow_buy"),
Some("missing_risk_state:unknown_fact"), Some("missing_risk_state:IS_PAUSED")] {
candidate.risk_level_code = code.map(str::to_owned);
assert_eq!(ChinaAShareRiskControl::selection_rejection_decision_with_config(
date, &candidate, &snapshot, Some(&instrument), &config), None);
}
candidate.risk_level_code = None;
assert_eq!(ChinaAShareRiskControl::buy_rejection_reason_with_config(
date, &candidate, &snapshot, None, 0.9, &config), Some("paused"));
assert_eq!(ChinaAShareRiskControl::sell_rejection_reason_with_config(
date, &candidate, &snapshot, None, None, 0.9, &config), Some("paused"));
let mut blacklist_only = config;
blacklist_only.static_rules.blacklisted_symbols.insert(candidate.symbol.to_string());
assert!(blacklist_only.static_rules.selection_checks_enabled());
assert_eq!(ChinaAShareRiskControl::selection_rejection_reason_with_config(
date, &candidate, &snapshot, None, &blacklist_only), Some("blacklisted"));
}
#[test] #[test]
fn one_yuan_buy_rule_uses_execution_price_not_later_close_or_earlier_open() { fn one_yuan_buy_rule_uses_execution_price_not_later_close_or_earlier_open() {
let day = d(2025, 2, 6); let day = d(2025, 2, 6);