diff --git a/crates/fidc-core/src/strategy.rs b/crates/fidc-core/src/strategy.rs index 85e3a12..7c97e51 100644 --- a/crates/fidc-core/src/strategy.rs +++ b/crates/fidc-core/src/strategy.rs @@ -1506,6 +1506,7 @@ 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(); diff --git a/crates/fidc-core/src/universe.rs b/crates/fidc-core/src/universe.rs index 1c132d7..c92b556 100644 --- a/crates/fidc-core/src/universe.rs +++ b/crates/fidc-core/src/universe.rs @@ -50,15 +50,16 @@ 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 = match self.risk_config { - Some(risk_config) => self + let eligible = match (self.risk_config, self.defer_selection_risk) { + (Some(risk_config), false) => self .data .eligible_universe_on_with_risk_config(self.decision_date, risk_config), - None => self.data.eligible_universe_on(self.decision_date).to_vec(), + _ => self.data.eligible_universe_on(self.decision_date).to_vec(), }; match self.dynamic_universe { Some(symbols) if !symbols.is_empty() => eligible @@ -406,6 +407,7 @@ mod tests { data: &data, dynamic_universe: None, risk_config: Some(&risk_config), + defer_selection_risk: false, }); let rules = diagnostics @@ -463,6 +465,7 @@ mod tests { data: &data, dynamic_universe: None, risk_config: Some(&risk_config), + defer_selection_risk: false, }); let selected_symbols = selected @@ -481,4 +484,61 @@ mod tests { assert!(rules.contains("st"), "{:?}", diagnostics.risk_decisions); assert!(rules.contains("kcb"), "{:?}", diagnostics.risk_decisions); } + + #[test] + fn selector_can_defer_configured_selection_risk_without_losing_diagnostics() { + let data = DataSet::from_components( + vec![ + instrument("000001.SZ"), + instrument("688001.SH"), + instrument("000002.SZ"), + ], + vec![ + market("000001.SZ", 10.0), + market("688001.SH", 10.0), + market("000002.SZ", 10.0), + ], + vec![ + factor("000001.SZ", 8.0), + factor("688001.SH", 9.0), + factor("000002.SZ", 10.0), + ], + vec![ + candidate("000001.SZ", true, false), + candidate("688001.SH", false, true), + candidate("000002.SZ", false, false), + ], + vec![benchmark()], + ) + .unwrap(); + let selector = DynamicMarketCapBandSelector::new(2000.0, 7.0, 10.0, 0.0, 10, 0.0, 0.0, 0.0); + let mut risk_config = FidcRiskControlConfig::default(); + risk_config.static_rules.reject_st_selection = true; + risk_config.static_rules.reject_kcb_selection = true; + + let (selected, diagnostics) = selector.select_with_diagnostics(&SelectionContext { + decision_date: d(), + benchmark: &benchmark(), + reference_level: 2000.0, + data: &data, + dynamic_universe: None, + risk_config: Some(&risk_config), + 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("000002.SZ")); + 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); + } }