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