From 29550dead2f7cc15ad6bb277ae7d5faa66bfe1fc Mon Sep 17 00:00:00 2001 From: boris Date: Sat, 29 Aug 2026 23:05:20 +0800 Subject: [PATCH] perf: reuse immutable fundamental universe --- .../fidc-core/src/platform_expr_strategy.rs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 21f6da6..d6e6b8c 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -8982,6 +8982,28 @@ impl PlatformExprStrategy { selection_risk_deferral: SelectionRiskDeferral, collect_risk_decisions: bool, ) -> (Vec, Vec) { + // The immutable DataSet already builds the same stable market-cap + // ordered fundamental universe. Reuse it when this strategy has no + // selection-stage risk or explicit universe exclusion. This preserves + // the full path for every strategy that enables a selection rule while + // avoiding a daily scan, candidate/market lookup, and stable sort for + // the common execution-risk-only contract. + if date == factor_date + && matches!(selection_risk_deferral, SelectionRiskDeferral::None) + && self.selection_risk_is_noop() + && self.normalized_universe_exclude.is_empty() + { + let base = ctx.data.eligible_universe_on(date); + let rows = if ctx.has_dynamic_universe() { + base.iter() + .filter(|row| ctx.dynamic_universe_contains(&row.symbol)) + .cloned() + .collect() + } else { + base.to_vec() + }; + return (rows, Vec::new()); + } let factor_rows = ctx.data.factor_snapshot_rows_on(factor_date); let mut rows = Vec::with_capacity(factor_rows.len()); let mut decisions = if collect_risk_decisions { @@ -9160,6 +9182,21 @@ impl PlatformExprStrategy { None } + fn selection_risk_is_noop(&self) -> bool { + let rules = &self.config.risk_config.static_rules; + !rules.reject_st_selection + && !rules.reject_star_st_selection + && !rules.reject_paused_selection + && !rules.reject_inactive_selection + && !rules.reject_new_listing_selection + && !rules.reject_kcb_selection + && !rules.reject_bjse_selection + && !rules.reject_one_yuan_selection + && !rules.reject_upper_limit_selection + && !rules.reject_lower_limit_selection + && (!rules.blacklist_enabled || rules.blacklisted_symbols.is_empty()) + } + fn stock_passes_universe_exclude( &self, _candidate: &crate::data::CandidateEligibility, @@ -13414,6 +13451,26 @@ mod tests { ); } + #[test] + fn selection_universe_cache_gate_only_accepts_pure_fundamental_contracts() { + let strategy = + PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); + assert!(strategy.selection_risk_is_noop()); + + let mut config = PlatformExprStrategyConfig::microcap_rotation(); + config.risk_config.static_rules.reject_st_selection = true; + assert!(!PlatformExprStrategy::new(config).selection_risk_is_noop()); + + let mut config = PlatformExprStrategyConfig::microcap_rotation(); + config.risk_config.static_rules.blacklisted_symbols.insert("000001.SZ".to_string()); + assert!(!PlatformExprStrategy::new(config).selection_risk_is_noop()); + + let mut config = PlatformExprStrategyConfig::microcap_rotation(); + config.risk_config.static_rules.blacklist_enabled = false; + config.risk_config.static_rules.blacklisted_symbols.insert("000001.SZ".to_string()); + assert!(PlatformExprStrategy::new(config).selection_risk_is_noop()); + } + #[test] fn platform_selection_uses_complete_static_risk_policy_before_universe_output() { let date = d(2025, 1, 2);