修正FIDC选股阶段风控语义

This commit is contained in:
boris
2026-07-05 03:08:38 +08:00
parent 8125ea2e3b
commit 549595c1c6
2 changed files with 31 additions and 50 deletions
+15 -30
View File
@@ -1506,7 +1506,6 @@ impl Strategy for CnSmallCapRotationStrategy {
data: ctx.data, data: ctx.data,
dynamic_universe: ctx.dynamic_universe, dynamic_universe: ctx.dynamic_universe,
risk_config: Some(&self.config.risk_config), risk_config: Some(&self.config.risk_config),
defer_selection_risk: ctx.is_lagged_execution(),
}); });
let before_ma_count = selected_before_ma.len(); let before_ma_count = selected_before_ma.len();
let mut ma_rejects = Vec::new(); let mut ma_rejects = Vec::new();
@@ -2474,11 +2473,7 @@ impl OmniMicroCapStrategy {
&self, &self,
ctx: &StrategyContext<'_>, ctx: &StrategyContext<'_>,
date: NaiveDate, date: NaiveDate,
defer_selection_risk: bool,
) -> Vec<FidcRiskDecisionAudit> { ) -> Vec<FidcRiskDecisionAudit> {
if defer_selection_risk {
return Vec::new();
}
let mut decisions = Vec::new(); let mut decisions = Vec::new();
for factor in ctx.data.factor_snapshots_on(date) { for factor in ctx.data.factor_snapshots_on(date) {
if ctx.has_dynamic_universe() && !ctx.dynamic_universe_contains(&factor.symbol) { if ctx.has_dynamic_universe() && !ctx.dynamic_universe_contains(&factor.symbol) {
@@ -2538,13 +2533,8 @@ impl OmniMicroCapStrategy {
date: NaiveDate, date: NaiveDate,
band_low: f64, band_low: f64,
band_high: f64, band_high: f64,
defer_selection_risk: bool,
) -> Result<(Vec<String>, Vec<String>), BacktestError> { ) -> Result<(Vec<String>, Vec<String>), BacktestError> {
let universe = if defer_selection_risk { let universe = ctx.eligible_universe_on_with_risk_config(date, &self.config.risk_config);
ctx.fundamental_universe_on(date)
} else {
ctx.eligible_universe_on_with_risk_config(date, &self.config.risk_config)
};
let mut diagnostics = Vec::new(); let mut diagnostics = Vec::new();
let mut selected = Vec::new(); let mut selected = Vec::new();
let start = lower_bound_eligible(&universe, band_low); let start = lower_bound_eligible(&universe, band_low);
@@ -2553,12 +2543,8 @@ impl OmniMicroCapStrategy {
if candidate.market_cap_bn > band_high { if candidate.market_cap_bn > band_high {
break; break;
} }
let rejection = if defer_selection_risk { let rejection = (!self.stock_passes_ma_filter(ctx, date, &candidate.symbol))
(!self.stock_passes_ma_filter(ctx, date, &candidate.symbol)) .then_some("ma_filter".to_string());
.then_some("ma_filter".to_string())
} else {
self.buy_rejection_reason(ctx, date, &candidate.symbol)?
};
if let Some(reason) = rejection { if let Some(reason) = rejection {
if diagnostics.len() < 12 { if diagnostics.len() < 12 {
diagnostics.push(format!("{} rejected by {}", candidate.symbol, reason)); diagnostics.push(format!("{} rejected by {}", candidate.symbol, reason));
@@ -2584,7 +2570,7 @@ impl Strategy for OmniMicroCapStrategy {
fn on_day(&mut self, ctx: &StrategyContext<'_>) -> Result<StrategyDecision, BacktestError> { fn on_day(&mut self, ctx: &StrategyContext<'_>) -> Result<StrategyDecision, BacktestError> {
let signal_date = ctx.decision_date; let signal_date = ctx.decision_date;
let execution_date = ctx.execution_date; let execution_date = ctx.execution_date;
let defer_selection_risk = ctx.is_lagged_execution(); let lagged_execution = ctx.is_lagged_execution();
if self.config.in_skip_window(signal_date) { if self.config.in_skip_window(signal_date) {
return Ok(StrategyDecision { return Ok(StrategyDecision {
rebalance: false, rebalance: false,
@@ -2630,8 +2616,8 @@ impl Strategy for OmniMicroCapStrategy {
// 使用前一交易日的指数价格计算市值区间(模拟实盘场景) // 使用前一交易日的指数价格计算市值区间(模拟实盘场景)
let (band_low, band_high) = self.market_cap_band(prev_index_level); let (band_low, band_high) = self.market_cap_band(prev_index_level);
let (stock_list, selection_notes) = let (stock_list, selection_notes) =
self.select_symbols(ctx, signal_date, band_low, band_high, defer_selection_risk)?; self.select_symbols(ctx, signal_date, band_low, band_high)?;
let risk_decisions = self.selection_risk_decisions(ctx, signal_date, defer_selection_risk); let risk_decisions = self.selection_risk_decisions(ctx, signal_date);
let periodic_rebalance = ctx.decision_index % self.config.refresh_rate == 0; let periodic_rebalance = ctx.decision_index % self.config.refresh_rate == 0;
let projection_date = signal_date; let projection_date = signal_date;
let mut projected = ctx.portfolio.clone(); let mut projected = ctx.portfolio.clone();
@@ -2656,8 +2642,8 @@ impl Strategy for OmniMicroCapStrategy {
<= position.average_cost * self.config.stop_loss_ratio <= position.average_cost * self.config.stop_loss_ratio
+ self.stop_loss_tolerance(market); + self.stop_loss_tolerance(market);
let profit_hit = current_price / position.average_cost > self.config.take_profit_ratio; let profit_hit = current_price / position.average_cost > self.config.take_profit_ratio;
let can_sell = defer_selection_risk let can_sell =
|| self.can_sell_position(ctx, execution_date, &position.symbol); lagged_execution || self.can_sell_position(ctx, execution_date, &position.symbol);
let at_upper_limit = market.is_at_upper_limit_price(current_price); let at_upper_limit = market.is_at_upper_limit_price(current_price);
if stop_hit || (profit_hit && !at_upper_limit) { if stop_hit || (profit_hit && !at_upper_limit) {
let sell_reason = if stop_hit { let sell_reason = if stop_hit {
@@ -2693,7 +2679,7 @@ impl Strategy for OmniMicroCapStrategy {
{ {
continue; continue;
} }
if !defer_selection_risk if !lagged_execution
&& self && self
.buy_rejection_reason(ctx, execution_date, symbol)? .buy_rejection_reason(ctx, execution_date, symbol)?
.is_some() .is_some()
@@ -2731,7 +2717,7 @@ impl Strategy for OmniMicroCapStrategy {
if stock_list.iter().any(|candidate| candidate == symbol) { if stock_list.iter().any(|candidate| candidate == symbol) {
continue; continue;
} }
if !defer_selection_risk && !self.can_sell_position(ctx, execution_date, symbol) { if !lagged_execution && !self.can_sell_position(ctx, execution_date, symbol) {
continue; continue;
} }
order_intents.push(OrderIntent::TargetValue { order_intents.push(OrderIntent::TargetValue {
@@ -2759,7 +2745,7 @@ impl Strategy for OmniMicroCapStrategy {
{ {
continue; continue;
} }
if !defer_selection_risk if !lagged_execution
&& self && self
.buy_rejection_reason(ctx, execution_date, symbol)? .buy_rejection_reason(ctx, execution_date, symbol)?
.is_some() .is_some()
@@ -2990,11 +2976,10 @@ mod tests {
default_cfg.stock_long_ma_days = 3; default_cfg.stock_long_ma_days = 3;
let default_strategy = OmniMicroCapStrategy::new(default_cfg.clone()); let default_strategy = OmniMicroCapStrategy::new(default_cfg.clone());
let (default_selected, _) = default_strategy let (default_selected, _) = default_strategy
.select_symbols(&ctx, dates[2], 0.0, 100.0, false) .select_symbols(&ctx, dates[2], 0.0, 100.0)
.expect("default selection"); .expect("default selection");
assert!(default_selected.is_empty()); assert!(default_selected.is_empty());
let default_risk_decisions = let default_risk_decisions = default_strategy.selection_risk_decisions(&ctx, dates[2]);
default_strategy.selection_risk_decisions(&ctx, dates[2], false);
assert_eq!(default_risk_decisions.len(), 1); assert_eq!(default_risk_decisions.len(), 1);
assert_eq!(default_risk_decisions[0].symbol, symbol); assert_eq!(default_risk_decisions[0].symbol, symbol);
assert_eq!(default_risk_decisions[0].rule_code, "kcb"); assert_eq!(default_risk_decisions[0].rule_code, "kcb");
@@ -3009,11 +2994,11 @@ mod tests {
cfg.risk_config.static_rules.reject_kcb_buy = false; cfg.risk_config.static_rules.reject_kcb_buy = false;
let configured_strategy = OmniMicroCapStrategy::new(cfg); let configured_strategy = OmniMicroCapStrategy::new(cfg);
let (selected, _) = configured_strategy let (selected, _) = configured_strategy
.select_symbols(&ctx, dates[2], 0.0, 100.0, false) .select_symbols(&ctx, dates[2], 0.0, 100.0)
.expect("configured selection"); .expect("configured selection");
assert!( assert!(
configured_strategy configured_strategy
.selection_risk_decisions(&ctx, dates[2], false) .selection_risk_decisions(&ctx, dates[2])
.is_empty() .is_empty()
); );
+16 -20
View File
@@ -50,20 +50,15 @@ pub struct SelectionContext<'a> {
pub data: &'a DataSet, pub data: &'a DataSet,
pub dynamic_universe: Option<&'a BTreeSet<String>>, pub dynamic_universe: Option<&'a BTreeSet<String>>,
pub risk_config: Option<&'a FidcRiskControlConfig>, pub risk_config: Option<&'a FidcRiskControlConfig>,
pub defer_selection_risk: bool,
} }
impl SelectionContext<'_> { impl SelectionContext<'_> {
fn eligible_universe(&self) -> Vec<EligibleUniverseSnapshot> { fn eligible_universe(&self) -> Vec<EligibleUniverseSnapshot> {
let eligible = if self.defer_selection_risk { let eligible = match self.risk_config {
self.data.fundamental_universe_on(self.decision_date) Some(risk_config) => self
} else { .data
match self.risk_config { .eligible_universe_on_with_risk_config(self.decision_date, risk_config),
Some(risk_config) => self None => self.data.eligible_universe_on(self.decision_date).to_vec(),
.data
.eligible_universe_on_with_risk_config(self.decision_date, risk_config),
None => self.data.eligible_universe_on(self.decision_date).to_vec(),
}
}; };
match self.dynamic_universe { match self.dynamic_universe {
Some(symbols) if !symbols.is_empty() => eligible Some(symbols) if !symbols.is_empty() => eligible
@@ -75,9 +70,6 @@ impl SelectionContext<'_> {
} }
fn selection_risk_decisions(&self) -> Vec<FidcRiskDecisionAudit> { fn selection_risk_decisions(&self) -> Vec<FidcRiskDecisionAudit> {
if self.defer_selection_risk {
return Vec::new();
}
let default_risk_config; let default_risk_config;
let risk_config = match self.risk_config { let risk_config = match self.risk_config {
Some(value) => value, Some(value) => value,
@@ -411,7 +403,6 @@ mod tests {
data: &data, data: &data,
dynamic_universe: None, dynamic_universe: None,
risk_config: Some(&FidcRiskControlConfig::default()), risk_config: Some(&FidcRiskControlConfig::default()),
defer_selection_risk: false,
}); });
let rules = diagnostics let rules = diagnostics
@@ -433,7 +424,7 @@ mod tests {
} }
#[test] #[test]
fn selector_defers_static_execution_risk_for_lagged_execution() { fn selector_applies_configured_selection_risk_on_decision_date() {
let data = DataSet::from_components( let data = DataSet::from_components(
vec![ vec![
instrument("000001.SZ"), instrument("000001.SZ"),
@@ -466,17 +457,22 @@ mod tests {
data: &data, data: &data,
dynamic_universe: None, dynamic_universe: None,
risk_config: Some(&FidcRiskControlConfig::default()), risk_config: Some(&FidcRiskControlConfig::default()),
defer_selection_risk: true,
}); });
let selected_symbols = selected let selected_symbols = selected
.iter() .iter()
.map(|candidate| candidate.symbol.as_str()) .map(|candidate| candidate.symbol.as_str())
.collect::<BTreeSet<_>>(); .collect::<BTreeSet<_>>();
assert!(selected_symbols.contains("000001.SZ")); assert!(!selected_symbols.contains("000001.SZ"));
assert!(selected_symbols.contains("688001.SH")); assert!(!selected_symbols.contains("688001.SH"));
assert!(selected_symbols.contains("000002.SZ")); assert!(selected_symbols.contains("000002.SZ"));
assert_eq!(diagnostics.not_eligible_count, 0); assert_eq!(diagnostics.not_eligible_count, 2);
assert!(diagnostics.risk_decisions.is_empty()); let rules = diagnostics
.risk_decisions
.iter()
.map(|decision| decision.rule_code.as_str())
.collect::<BTreeSet<_>>();
assert!(rules.contains("st"), "{:?}", diagnostics.risk_decisions);
assert!(rules.contains("kcb"), "{:?}", diagnostics.risk_decisions);
} }
} }