From df7a1ee382843b22d145718576d24e78fdd6c804 Mon Sep 17 00:00:00 2001 From: boris Date: Mon, 7 Sep 2026 10:50:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=89=E5=85=B1=E4=BA=AB=E5=B8=82=E5=80=BC?= =?UTF-8?q?=E7=B4=A2=E5=BC=95=E8=A3=81=E5=89=AA=E5=80=99=E9=80=89=E5=8C=BA?= =?UTF-8?q?=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 112 +++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 40463cd..ec41c4f 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -10370,6 +10370,41 @@ impl PlatformExprStrategy { && matches!(self.config.rank_by.trim(), "market_cap" | "market_cap_bn") } + fn market_cap_ordered_selection_range( + &self, + factor_day: &DailySnapshotView<'_>, + symbol_ids: &[u32], + band_low: f64, + band_high: f64, + ) -> std::ops::Range { + if !matches!( + self.config.market_cap_field.as_str(), + "market_cap" + | "market_cap_bn" + | "candidate_market_cap" + | "candidate_market_cap_bn" + ) || !band_low.is_finite() + || !band_high.is_finite() + { + return 0..symbol_ids.len(); + } + if band_low > band_high { + return 0..0; + } + let market_cap = |symbol_id| { + decision_market_cap_bn( + factor_day + .factor(symbol_id) + .expect("market-cap order references missing factor row"), + ) + }; + let start = symbol_ids.partition_point(|symbol_id| market_cap(*symbol_id) < band_low); + let end = start + + symbol_ids[start..] + .partition_point(|symbol_id| market_cap(*symbol_id) <= band_high); + start..end + } + fn selection_candidate_passes_filters( &self, ctx: &StrategyContext<'_>, @@ -10618,8 +10653,15 @@ impl PlatformExprStrategy { let execution_day = ctx.data.daily_snapshot_view(date); let universe_factor_day = ctx.data.daily_snapshot_view(universe_factor_date); let factor_day = ctx.data.daily_snapshot_view(stock_factor_date); - let mut selected = Vec::with_capacity(limit.min(universe_symbol_ids.len())); - for symbol_id in universe_symbol_ids { + let candidate_range = self.market_cap_ordered_selection_range( + &universe_factor_day, + &universe_symbol_ids, + band_low, + band_high, + ); + let candidate_symbol_ids = &universe_symbol_ids[candidate_range]; + let mut selected = Vec::with_capacity(limit.min(candidate_symbol_ids.len())); + for &symbol_id in candidate_symbol_ids { let factor = universe_factor_day .factor(symbol_id) .expect("market-cap order references missing factor row"); @@ -15934,6 +15976,72 @@ mod tests { assert!(!PlatformExprStrategy::new(cfg).rank_reuses_market_cap_order()); } + #[test] + fn market_cap_ordered_selection_range_is_inclusive_and_field_specific() { + let date = d(2025, 1, 2); + let data = DataSet::from_components( + Vec::new(), + Vec::new(), + [ + ("000004.SZ", 40.0), + ("000002.SZ", 10.0), + ("000003.SZ", 20.0), + ("000001.SZ", 5.0), + ] + .into_iter() + .map(|(symbol, market_cap_bn)| DailyFactorSnapshot { + date, + symbol: symbol.to_string(), + market_cap_bn, + free_float_cap_bn: market_cap_bn / 2.0, + pe_ttm: 10.0, + turnover_ratio: Some(1.0), + effective_turnover_ratio: Some(1.0), + extra_factors: BTreeMap::new(), + }) + .collect(), + Vec::new(), + vec![BenchmarkSnapshot { + date, + benchmark: "000300.SH".to_string(), + open: 1000.0, + close: 1000.0, + prev_close: 1000.0, + volume: 1_000_000, + }], + ) + .expect("factor-only data"); + let factor_day = data.daily_snapshot_view(date); + let symbol_ids = data.factor_symbol_ids_by_market_cap_on(date); + let mut cfg = PlatformExprStrategyConfig::generic(); + cfg.market_cap_field = "market_cap".to_string(); + let strategy = PlatformExprStrategy::new(cfg.clone()); + + let range = strategy.market_cap_ordered_selection_range( + &factor_day, + symbol_ids, + 10.0, + 20.0, + ); + let selected_caps = symbol_ids[range] + .iter() + .map(|symbol_id| factor_day.factor(*symbol_id).expect("factor row").market_cap_bn) + .collect::>(); + assert_eq!(selected_caps, vec![10.0, 20.0]); + + assert!( + strategy + .market_cap_ordered_selection_range(&factor_day, symbol_ids, 20.0, 10.0) + .is_empty() + ); + cfg.market_cap_field = "free_float_cap".to_string(); + assert_eq!( + PlatformExprStrategy::new(cfg) + .market_cap_ordered_selection_range(&factor_day, symbol_ids, 10.0, 20.0), + 0..symbol_ids.len() + ); + } + #[test] fn current_rolling_helpers_do_not_load_factor_maps_or_decision_rollings() { let mut cfg = PlatformExprStrategyConfig::microcap_rotation();