From e77baffa10bae2661ea7315ec15acc5563c61d38 Mon Sep 17 00:00:00 2001 From: boris Date: Fri, 3 Jul 2026 06:26:51 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90FIDC=E5=8C=97=E4=BA=A4?= =?UTF-8?q?=E6=89=80=E9=A3=8E=E6=8E=A7=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_strategy_spec.rs | 16 +++++ crates/fidc-core/src/risk_control.rs | 64 ++++++++++++++++++- crates/fidc-core/tests/explicit_order_flow.rs | 5 +- 3 files changed, 82 insertions(+), 3 deletions(-) diff --git a/crates/fidc-core/src/platform_strategy_spec.rs b/crates/fidc-core/src/platform_strategy_spec.rs index ddbd8a1..b63d6da 100644 --- a/crates/fidc-core/src/platform_strategy_spec.rs +++ b/crates/fidc-core/src/platform_strategy_spec.rs @@ -216,6 +216,10 @@ pub struct StrategyRiskPolicySpec { pub reject_kcb_selection: Option, #[serde(default, alias = "reject_kcb_buy")] pub reject_kcb_buy: Option, + #[serde(default, alias = "reject_bjse_selection")] + pub reject_bjse_selection: Option, + #[serde(default, alias = "reject_bjse_buy")] + pub reject_bjse_buy: Option, #[serde(default, alias = "reject_one_yuan_selection")] pub reject_one_yuan_selection: Option, #[serde(default, alias = "reject_one_yuan_buy")] @@ -285,6 +289,8 @@ const RISK_POLICY_BOOL_ALIAS_GROUPS: &[(&str, &[&str])] = &[ ("rejectNewListingBuy", &["reject_new_listing_buy"]), ("rejectKcbSelection", &["reject_kcb_selection"]), ("rejectKcbBuy", &["reject_kcb_buy"]), + ("rejectBjseSelection", &["reject_bjse_selection"]), + ("rejectBjseBuy", &["reject_bjse_buy"]), ("rejectOneYuanSelection", &["reject_one_yuan_selection"]), ("rejectOneYuanBuy", &["reject_one_yuan_buy"]), ("respectAllowBuySell", &["respect_allow_buy_sell"]), @@ -794,6 +800,12 @@ fn apply_risk_policy_overrides( if let Some(value) = policy.reject_kcb_buy { 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 { static_rules.reject_one_yuan_selection = value; } @@ -2244,6 +2256,8 @@ mod tests { "rejectStarStBuy": true, "reject_paused_selection": false, "reject_kcb_buy": false, + "rejectBjseSelection": false, + "reject_bjse_buy": false, "blacklisted_symbols": ["000001.SZ"], "blacklist": ["000002.SZ", "000001.SZ"], "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_paused_selection); 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!( cfg.risk_config .static_rules diff --git a/crates/fidc-core/src/risk_control.rs b/crates/fidc-core/src/risk_control.rs index 6280a22..4a50f9f 100644 --- a/crates/fidc-core/src/risk_control.rs +++ b/crates/fidc-core/src/risk_control.rs @@ -25,6 +25,8 @@ pub struct StaticRiskRuleConfig { pub reject_new_listing_buy: bool, pub reject_kcb_selection: 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_buy: bool, pub respect_allow_buy_sell: bool, @@ -53,6 +55,8 @@ impl Default for StaticRiskRuleConfig { reject_new_listing_buy: true, reject_kcb_selection: true, reject_kcb_buy: true, + reject_bjse_selection: true, + reject_bjse_buy: true, reject_one_yuan_selection: true, reject_one_yuan_buy: true, respect_allow_buy_sell: true, @@ -358,6 +362,14 @@ impl ChinaAShareRiskControl { if reject_kcb && (candidate.is_kcb || symbol_is_kcb(&candidate.symbol)) { 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 { RiskCheckScope::Selection => config.static_rules.reject_one_yuan_selection, 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") } +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 { let Some((_, fields)) = code.split_once(':') else { 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_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 @@ -550,6 +568,7 @@ fn missing_buy_risk_state_rejected(code: &str, config: &FidcRiskControlConfig) - || config.static_rules.reject_inactive_buy || config.static_rules.reject_new_listing_buy || config.static_rules.reject_kcb_buy + || config.static_rules.reject_bjse_buy || config.static_rules.reject_one_yuan_buy || config.static_rules.reject_upper_limit_buy || config.static_rules.respect_allow_buy_sell; @@ -606,8 +625,18 @@ fn missing_single_field_rejected( RiskCheckScope::Sell => false, }, "is_kcb" | "kcb" | "board" | "market_board" => match scope { - RiskCheckScope::Selection => config.static_rules.reject_kcb_selection, - RiskCheckScope::Buy => config.static_rules.reject_kcb_buy, + RiskCheckScope::Selection => { + 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, }, "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_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 @@ -655,6 +685,7 @@ fn missing_single_field_rejected( || config.static_rules.reject_inactive_buy || config.static_rules.reject_new_listing_buy || config.static_rules.reject_kcb_buy + || config.static_rules.reject_bjse_buy || config.static_rules.reject_one_yuan_buy || config.static_rules.reject_upper_limit_buy || config.static_rules.respect_allow_buy_sell @@ -813,6 +844,33 @@ mod tests { 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] fn concrete_kcb_reason_wins_over_generic_missing_risk_state() { let date = d(2025, 1, 2); @@ -994,6 +1052,8 @@ mod tests { config.static_rules.reject_new_listing_buy = false; config.static_rules.reject_kcb_selection = 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_buy = false; config.static_rules.respect_allow_buy_sell = false; diff --git a/crates/fidc-core/tests/explicit_order_flow.rs b/crates/fidc-core/tests/explicit_order_flow.rs index 2c3d1d0..cd0abd6 100644 --- a/crates/fidc-core/tests/explicit_order_flow.rs +++ b/crates/fidc-core/tests/explicit_order_flow.rs @@ -3865,11 +3865,14 @@ fn broker_allows_bjse_quantities_above_minimum_without_round_lot_step() { ) .expect("dataset"); 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( ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), PriceField::Open, - ); + ) + .with_risk_config(risk_config); let report = broker .execute(