补齐FIDC北交所风控开关

This commit is contained in:
boris
2026-07-03 06:26:51 +08:00
parent 179c4eaff5
commit e77baffa10
3 changed files with 82 additions and 3 deletions
@@ -216,6 +216,10 @@ pub struct StrategyRiskPolicySpec {
pub reject_kcb_selection: Option<bool>, pub reject_kcb_selection: Option<bool>,
#[serde(default, alias = "reject_kcb_buy")] #[serde(default, alias = "reject_kcb_buy")]
pub reject_kcb_buy: Option<bool>, pub reject_kcb_buy: Option<bool>,
#[serde(default, alias = "reject_bjse_selection")]
pub reject_bjse_selection: Option<bool>,
#[serde(default, alias = "reject_bjse_buy")]
pub reject_bjse_buy: Option<bool>,
#[serde(default, alias = "reject_one_yuan_selection")] #[serde(default, alias = "reject_one_yuan_selection")]
pub reject_one_yuan_selection: Option<bool>, pub reject_one_yuan_selection: Option<bool>,
#[serde(default, alias = "reject_one_yuan_buy")] #[serde(default, alias = "reject_one_yuan_buy")]
@@ -285,6 +289,8 @@ const RISK_POLICY_BOOL_ALIAS_GROUPS: &[(&str, &[&str])] = &[
("rejectNewListingBuy", &["reject_new_listing_buy"]), ("rejectNewListingBuy", &["reject_new_listing_buy"]),
("rejectKcbSelection", &["reject_kcb_selection"]), ("rejectKcbSelection", &["reject_kcb_selection"]),
("rejectKcbBuy", &["reject_kcb_buy"]), ("rejectKcbBuy", &["reject_kcb_buy"]),
("rejectBjseSelection", &["reject_bjse_selection"]),
("rejectBjseBuy", &["reject_bjse_buy"]),
("rejectOneYuanSelection", &["reject_one_yuan_selection"]), ("rejectOneYuanSelection", &["reject_one_yuan_selection"]),
("rejectOneYuanBuy", &["reject_one_yuan_buy"]), ("rejectOneYuanBuy", &["reject_one_yuan_buy"]),
("respectAllowBuySell", &["respect_allow_buy_sell"]), ("respectAllowBuySell", &["respect_allow_buy_sell"]),
@@ -794,6 +800,12 @@ fn apply_risk_policy_overrides(
if let Some(value) = policy.reject_kcb_buy { if let Some(value) = policy.reject_kcb_buy {
static_rules.reject_kcb_buy = value; static_rules.reject_kcb_buy = value;
} }
if let Some(value) = policy.reject_bjse_selection {
static_rules.reject_bjse_selection = value;
}
if let Some(value) = policy.reject_bjse_buy {
static_rules.reject_bjse_buy = value;
}
if let Some(value) = policy.reject_one_yuan_selection { if let Some(value) = policy.reject_one_yuan_selection {
static_rules.reject_one_yuan_selection = value; static_rules.reject_one_yuan_selection = value;
} }
@@ -2244,6 +2256,8 @@ mod tests {
"rejectStarStBuy": true, "rejectStarStBuy": true,
"reject_paused_selection": false, "reject_paused_selection": false,
"reject_kcb_buy": false, "reject_kcb_buy": false,
"rejectBjseSelection": false,
"reject_bjse_buy": false,
"blacklisted_symbols": ["000001.SZ"], "blacklisted_symbols": ["000001.SZ"],
"blacklist": ["000002.SZ", "000001.SZ"], "blacklist": ["000002.SZ", "000001.SZ"],
"volume_limit_enabled": true, "volume_limit_enabled": true,
@@ -2266,6 +2280,8 @@ mod tests {
assert!(cfg.risk_config.static_rules.reject_st_buy); assert!(cfg.risk_config.static_rules.reject_st_buy);
assert!(!cfg.risk_config.static_rules.reject_paused_selection); assert!(!cfg.risk_config.static_rules.reject_paused_selection);
assert!(!cfg.risk_config.static_rules.reject_kcb_buy); assert!(!cfg.risk_config.static_rules.reject_kcb_buy);
assert!(!cfg.risk_config.static_rules.reject_bjse_selection);
assert!(!cfg.risk_config.static_rules.reject_bjse_buy);
assert!( assert!(
cfg.risk_config cfg.risk_config
.static_rules .static_rules
+62 -2
View File
@@ -25,6 +25,8 @@ pub struct StaticRiskRuleConfig {
pub reject_new_listing_buy: bool, pub reject_new_listing_buy: bool,
pub reject_kcb_selection: bool, pub reject_kcb_selection: bool,
pub reject_kcb_buy: bool, pub reject_kcb_buy: bool,
pub reject_bjse_selection: bool,
pub reject_bjse_buy: bool,
pub reject_one_yuan_selection: bool, pub reject_one_yuan_selection: bool,
pub reject_one_yuan_buy: bool, pub reject_one_yuan_buy: bool,
pub respect_allow_buy_sell: bool, pub respect_allow_buy_sell: bool,
@@ -53,6 +55,8 @@ impl Default for StaticRiskRuleConfig {
reject_new_listing_buy: true, reject_new_listing_buy: true,
reject_kcb_selection: true, reject_kcb_selection: true,
reject_kcb_buy: true, reject_kcb_buy: true,
reject_bjse_selection: true,
reject_bjse_buy: true,
reject_one_yuan_selection: true, reject_one_yuan_selection: true,
reject_one_yuan_buy: true, reject_one_yuan_buy: true,
respect_allow_buy_sell: true, respect_allow_buy_sell: true,
@@ -358,6 +362,14 @@ impl ChinaAShareRiskControl {
if reject_kcb && (candidate.is_kcb || symbol_is_kcb(&candidate.symbol)) { if reject_kcb && (candidate.is_kcb || symbol_is_kcb(&candidate.symbol)) {
return Some("kcb"); return Some("kcb");
} }
let reject_bjse = match scope {
RiskCheckScope::Selection => config.static_rules.reject_bjse_selection,
RiskCheckScope::Buy => config.static_rules.reject_bjse_buy,
RiskCheckScope::Sell => false,
};
if reject_bjse && symbol_is_bjse(&candidate.symbol) {
return Some("bjse");
}
let reject_one_yuan = match scope { let reject_one_yuan = match scope {
RiskCheckScope::Selection => config.static_rules.reject_one_yuan_selection, RiskCheckScope::Selection => config.static_rules.reject_one_yuan_selection,
RiskCheckScope::Buy => config.static_rules.reject_one_yuan_buy, RiskCheckScope::Buy => config.static_rules.reject_one_yuan_buy,
@@ -514,6 +526,11 @@ fn symbol_is_kcb(symbol: &str) -> bool {
(normalized.starts_with("688") || normalized.starts_with("689")) && normalized.ends_with(".SH") (normalized.starts_with("688") || normalized.starts_with("689")) && normalized.ends_with(".SH")
} }
fn symbol_is_bjse(symbol: &str) -> bool {
let normalized = symbol.trim().to_ascii_uppercase();
normalized.ends_with(".BJ") || normalized.ends_with(".BSE") || normalized.ends_with(".BE")
}
fn missing_risk_state_fields(code: &str) -> Vec<String> { fn missing_risk_state_fields(code: &str) -> Vec<String> {
let Some((_, fields)) = code.split_once(':') else { let Some((_, fields)) = code.split_once(':') else {
return Vec::new(); return Vec::new();
@@ -534,6 +551,7 @@ fn missing_selection_risk_state_rejected(code: &str, config: &FidcRiskControlCon
|| config.static_rules.reject_inactive_selection || config.static_rules.reject_inactive_selection
|| config.static_rules.reject_new_listing_selection || config.static_rules.reject_new_listing_selection
|| config.static_rules.reject_kcb_selection || config.static_rules.reject_kcb_selection
|| config.static_rules.reject_bjse_selection
|| config.static_rules.reject_one_yuan_selection || config.static_rules.reject_one_yuan_selection
|| config.static_rules.reject_upper_limit_selection || config.static_rules.reject_upper_limit_selection
|| config.static_rules.reject_lower_limit_selection || config.static_rules.reject_lower_limit_selection
@@ -550,6 +568,7 @@ fn missing_buy_risk_state_rejected(code: &str, config: &FidcRiskControlConfig) -
|| config.static_rules.reject_inactive_buy || config.static_rules.reject_inactive_buy
|| config.static_rules.reject_new_listing_buy || config.static_rules.reject_new_listing_buy
|| config.static_rules.reject_kcb_buy || config.static_rules.reject_kcb_buy
|| config.static_rules.reject_bjse_buy
|| config.static_rules.reject_one_yuan_buy || config.static_rules.reject_one_yuan_buy
|| config.static_rules.reject_upper_limit_buy || config.static_rules.reject_upper_limit_buy
|| config.static_rules.respect_allow_buy_sell; || config.static_rules.respect_allow_buy_sell;
@@ -606,8 +625,18 @@ fn missing_single_field_rejected(
RiskCheckScope::Sell => false, RiskCheckScope::Sell => false,
}, },
"is_kcb" | "kcb" | "board" | "market_board" => match scope { "is_kcb" | "kcb" | "board" | "market_board" => match scope {
RiskCheckScope::Selection => config.static_rules.reject_kcb_selection, RiskCheckScope::Selection => {
RiskCheckScope::Buy => config.static_rules.reject_kcb_buy, config.static_rules.reject_kcb_selection
|| config.static_rules.reject_bjse_selection
}
RiskCheckScope::Buy => {
config.static_rules.reject_kcb_buy || config.static_rules.reject_bjse_buy
}
RiskCheckScope::Sell => false,
},
"is_bjse" | "bjse" | "bse" | "beijing_exchange" | "north_exchange" => match scope {
RiskCheckScope::Selection => config.static_rules.reject_bjse_selection,
RiskCheckScope::Buy => config.static_rules.reject_bjse_buy,
RiskCheckScope::Sell => false, RiskCheckScope::Sell => false,
}, },
"is_one_yuan" | "one_yuan" => match scope { "is_one_yuan" | "one_yuan" => match scope {
@@ -644,6 +673,7 @@ fn missing_single_field_rejected(
|| config.static_rules.reject_inactive_selection || config.static_rules.reject_inactive_selection
|| config.static_rules.reject_new_listing_selection || config.static_rules.reject_new_listing_selection
|| config.static_rules.reject_kcb_selection || config.static_rules.reject_kcb_selection
|| config.static_rules.reject_bjse_selection
|| config.static_rules.reject_one_yuan_selection || config.static_rules.reject_one_yuan_selection
|| config.static_rules.reject_upper_limit_selection || config.static_rules.reject_upper_limit_selection
|| config.static_rules.reject_lower_limit_selection || config.static_rules.reject_lower_limit_selection
@@ -655,6 +685,7 @@ fn missing_single_field_rejected(
|| config.static_rules.reject_inactive_buy || config.static_rules.reject_inactive_buy
|| config.static_rules.reject_new_listing_buy || config.static_rules.reject_new_listing_buy
|| config.static_rules.reject_kcb_buy || config.static_rules.reject_kcb_buy
|| config.static_rules.reject_bjse_buy
|| config.static_rules.reject_one_yuan_buy || config.static_rules.reject_one_yuan_buy
|| config.static_rules.reject_upper_limit_buy || config.static_rules.reject_upper_limit_buy
|| config.static_rules.respect_allow_buy_sell || config.static_rules.respect_allow_buy_sell
@@ -813,6 +844,33 @@ mod tests {
assert_eq!(configured_reason, None); assert_eq!(configured_reason, None);
} }
#[test]
fn configurable_bjse_filter_can_be_disabled() {
let date = d(2025, 1, 2);
let mut candidate = candidate(date);
candidate.symbol = "430047.BJ".to_string();
candidate.allow_sell = true;
let market = market(date, 6.27, 5.63);
let default_selection =
ChinaAShareRiskControl::selection_rejection_reason(date, &candidate, &market, None);
let default_buy =
ChinaAShareRiskControl::buy_rejection_reason(date, &candidate, &market, None, 6.27);
let mut config = FidcRiskControlConfig::default();
config.static_rules.reject_bjse_selection = false;
config.static_rules.reject_bjse_buy = false;
let configured_selection = ChinaAShareRiskControl::selection_rejection_reason_with_config(
date, &candidate, &market, None, &config,
);
let configured_buy = ChinaAShareRiskControl::buy_rejection_reason_with_config(
date, &candidate, &market, None, 6.27, &config,
);
assert_eq!(default_selection, Some("bjse"));
assert_eq!(default_buy, Some("bjse"));
assert_eq!(configured_selection, None);
assert_eq!(configured_buy, None);
}
#[test] #[test]
fn concrete_kcb_reason_wins_over_generic_missing_risk_state() { fn concrete_kcb_reason_wins_over_generic_missing_risk_state() {
let date = d(2025, 1, 2); let date = d(2025, 1, 2);
@@ -994,6 +1052,8 @@ mod tests {
config.static_rules.reject_new_listing_buy = false; config.static_rules.reject_new_listing_buy = false;
config.static_rules.reject_kcb_selection = false; config.static_rules.reject_kcb_selection = false;
config.static_rules.reject_kcb_buy = false; config.static_rules.reject_kcb_buy = false;
config.static_rules.reject_bjse_selection = false;
config.static_rules.reject_bjse_buy = false;
config.static_rules.reject_one_yuan_selection = false; config.static_rules.reject_one_yuan_selection = false;
config.static_rules.reject_one_yuan_buy = false; config.static_rules.reject_one_yuan_buy = false;
config.static_rules.respect_allow_buy_sell = false; config.static_rules.respect_allow_buy_sell = false;
@@ -3865,11 +3865,14 @@ fn broker_allows_bjse_quantities_above_minimum_without_round_lot_step() {
) )
.expect("dataset"); .expect("dataset");
let mut portfolio = PortfolioState::new(1_010.0); let mut portfolio = PortfolioState::new(1_010.0);
let mut risk_config = FidcRiskControlConfig::default();
risk_config.static_rules.reject_bjse_buy = false;
let broker = BrokerSimulator::new_with_execution_price( let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(), ChinaAShareCostModel::default(),
ChinaEquityRuleHooks::default(), ChinaEquityRuleHooks::default(),
PriceField::Open, PriceField::Open,
); )
.with_risk_config(risk_config);
let report = broker let report = broker
.execute( .execute(