补齐FIDC北交所风控开关
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user