From 0e3c2028d0b0a26a2d3b8e2fb9dd0765c60809b1 Mon Sep 17 00:00:00 2001 From: boris Date: Sat, 5 Sep 2026 04:44:53 +0800 Subject: [PATCH] perf: stream selection candidates by symbol id --- .../fidc-core/src/platform_expr_strategy.rs | 255 +++++++++++++++--- 1 file changed, 211 insertions(+), 44 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index cebd83b..0fe438f 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -4007,6 +4007,35 @@ impl PlatformExprStrategy { ) } + #[allow(clippy::too_many_arguments)] + fn selection_stock_state_with_factor_date_from_views_by_symbol_id<'a>( + &self, + ctx: &StrategyContext<'a>, + date: NaiveDate, + factor_date: NaiveDate, + symbol_id: u32, + symbol: &str, + execution_day: &DailySnapshotView<'a>, + factor_day: &DailySnapshotView<'a>, + ) -> Result, BacktestError> { + let source = ViewStockStateSnapshotSource { + execution: execution_day, + factor: factor_day, + same_date: factor_date == date, + }; + let use_intraday_quote = self.selection_quote_usage != StockFilterQuoteUsage::DailyOnly; + self.stock_state_with_factor_date_and_time_from_source_by_symbol_id( + ctx, + date, + factor_date, + symbol_id, + symbol, + None, + use_intraday_quote, + &source, + ) + } + fn stock_decision_rolling_mean( &self, ctx: &StrategyContext<'_>, @@ -4149,6 +4178,33 @@ impl PlatformExprStrategy { symbol: symbol.to_string(), }) })?; + self.stock_state_with_factor_date_and_time_from_source_by_symbol_id( + ctx, + date, + factor_date, + symbol_id, + symbol, + execution_time, + use_intraday_quote, + source, + ) + } + + #[allow(clippy::too_many_arguments)] + fn stock_state_with_factor_date_and_time_from_source_by_symbol_id<'a, S>( + &self, + ctx: &StrategyContext<'a>, + date: NaiveDate, + factor_date: NaiveDate, + symbol_id: u32, + symbol: &str, + execution_time: Option, + use_intraday_quote: bool, + source: &S, + ) -> Result, BacktestError> + where + S: StockStateSnapshotSource<'a>, + { let shared_symbol = ctx.data.shared_symbol_by_id(symbol_id).ok_or_else(|| { BacktestError::Data(crate::data::DataSetError::MissingSnapshot { kind: "symbol_reverse_index", @@ -9371,13 +9427,17 @@ impl PlatformExprStrategy { } fn field_value(&self, row: &EligibleUniverseSnapshot) -> f64 { + self.field_value_from_caps(row.market_cap_bn, row.free_float_cap_bn) + } + + fn field_value_from_caps(&self, market_cap_bn: f64, free_float_cap_bn: f64) -> f64 { match self.config.market_cap_field.as_str() { - "market_cap_bn" => row.market_cap_bn, + "market_cap_bn" => market_cap_bn, "free_float_cap" | "free_float_market_cap" => { - Self::market_cap_storage_to_strategy_unit(row.free_float_cap_bn) + Self::market_cap_storage_to_strategy_unit(free_float_cap_bn) } - "free_float_cap_bn" => row.free_float_cap_bn, - _ => Self::market_cap_storage_to_strategy_unit(row.market_cap_bn), + "free_float_cap_bn" => free_float_cap_bn, + _ => Self::market_cap_storage_to_strategy_unit(market_cap_bn), } } @@ -9421,7 +9481,39 @@ impl PlatformExprStrategy { selection_risk_deferral: SelectionRiskDeferral, collect_risk_decisions: bool, ) -> (Vec, Vec) { - let mut rows = Vec::new(); + let (symbol_ids, decisions) = self.selection_symbol_ids_and_risk_decisions_with_options( + ctx, + date, + factor_date, + selection_risk_deferral, + collect_risk_decisions, + ); + let factor_day = ctx.data.daily_snapshot_view(factor_date); + let rows = symbol_ids + .into_iter() + .map(|symbol_id| { + let factor = factor_day + .factor(symbol_id) + .expect("market-cap order references missing factor row"); + EligibleUniverseSnapshot { + symbol: factor.symbol.clone(), + market_cap_bn: decision_market_cap_bn(factor), + free_float_cap_bn: decision_free_float_cap_bn(factor), + } + }) + .collect(); + (rows, decisions) + } + + fn selection_symbol_ids_and_risk_decisions_with_options( + &self, + ctx: &StrategyContext<'_>, + date: NaiveDate, + factor_date: NaiveDate, + selection_risk_deferral: SelectionRiskDeferral, + collect_risk_decisions: bool, + ) -> (Vec, Vec) { + let mut symbol_ids = Vec::new(); let mut decisions = Vec::new(); let mut eligible_symbols = vec![false; ctx.data.symbol_count()]; let execution_day = ctx.data.daily_snapshot_view(date); @@ -9504,16 +9596,9 @@ impl PlatformExprStrategy { { continue; } - let factor = factor_day - .factor(symbol_id) - .expect("market-cap order references missing factor row"); - rows.push(EligibleUniverseSnapshot { - symbol: factor.symbol.clone(), - market_cap_bn: decision_market_cap_bn(factor), - free_float_cap_bn: decision_free_float_cap_bn(factor), - }); + symbol_ids.push(symbol_id); } - (rows, decisions) + (symbol_ids, decisions) } #[cfg(test)] @@ -9657,6 +9742,21 @@ impl PlatformExprStrategy { candidate: &EligibleUniverseSnapshot, stock: &StockExpressionState, field: &str, + ) -> Option { + self.stock_numeric_field_value_from_caps( + candidate.market_cap_bn, + candidate.free_float_cap_bn, + stock, + field, + ) + } + + fn stock_numeric_field_value_from_caps( + &self, + candidate_market_cap_bn: f64, + candidate_free_float_cap_bn: f64, + stock: &StockExpressionState, + field: &str, ) -> Option { match field { "market_cap" => Some(stock.market_cap), @@ -9722,13 +9822,13 @@ impl PlatformExprStrategy { "is_one_yuan" => Some(if stock.is_one_yuan { 1.0 } else { 0.0 }), "is_new_listing" => Some(if stock.is_new_listing { 1.0 } else { 0.0 }), "candidate_market_cap" => Some(Self::market_cap_storage_to_strategy_unit( - candidate.market_cap_bn, + candidate_market_cap_bn, )), - "candidate_market_cap_bn" => Some(candidate.market_cap_bn), + "candidate_market_cap_bn" => Some(candidate_market_cap_bn), "candidate_free_float_cap" => Some(Self::market_cap_storage_to_strategy_unit( - candidate.free_float_cap_bn, + candidate_free_float_cap_bn, )), - "candidate_free_float_cap_bn" => Some(candidate.free_float_cap_bn), + "candidate_free_float_cap_bn" => Some(candidate_free_float_cap_bn), other => stock.extra_factors.get(other).copied(), } } @@ -9737,20 +9837,38 @@ impl PlatformExprStrategy { &self, candidate: &EligibleUniverseSnapshot, stock: &StockExpressionState, + ) -> f64 { + self.selection_field_value_from_caps( + candidate.market_cap_bn, + candidate.free_float_cap_bn, + stock, + ) + } + + fn selection_field_value_from_caps( + &self, + market_cap_bn: f64, + free_float_cap_bn: f64, + stock: &StockExpressionState, ) -> f64 { match self.config.market_cap_field.as_str() { "market_cap" => { - return Self::market_cap_storage_to_strategy_unit(candidate.market_cap_bn); + return Self::market_cap_storage_to_strategy_unit(market_cap_bn); } - "market_cap_bn" => return candidate.market_cap_bn, + "market_cap_bn" => return market_cap_bn, "free_float_cap" | "free_float_market_cap" => { - return Self::market_cap_storage_to_strategy_unit(candidate.free_float_cap_bn); + return Self::market_cap_storage_to_strategy_unit(free_float_cap_bn); } - "free_float_cap_bn" => return candidate.free_float_cap_bn, + "free_float_cap_bn" => return free_float_cap_bn, _ => {} } - self.stock_numeric_field_value(candidate, stock, self.config.market_cap_field.as_str()) - .unwrap_or_else(|| self.field_value(candidate)) + self.stock_numeric_field_value_from_caps( + market_cap_bn, + free_float_cap_bn, + stock, + self.config.market_cap_field.as_str(), + ) + .unwrap_or_else(|| self.field_value_from_caps(market_cap_bn, free_float_cap_bn)) } fn rank_value( @@ -9800,7 +9918,7 @@ impl PlatformExprStrategy { ctx: &StrategyContext<'_>, date: NaiveDate, day: &DayExpressionState, - candidate: &EligibleUniverseSnapshot, + symbol: &str, stock: &StockExpressionState, diagnostics: &mut Vec, ) -> Result { @@ -9808,13 +9926,13 @@ impl PlatformExprStrategy { && let Some(reason) = self.stock_selection_limit_rejection_reason(stock) { if diagnostics.len() < 12 { - diagnostics.push(format!("{} rejected by {}", candidate.symbol, reason)); + diagnostics.push(format!("{} rejected by {}", symbol, reason)); } return Ok(false); } if !self.stock_passes_expr(ctx, day, stock)? { if diagnostics.len() < 12 { - diagnostics.push(format!("{} rejected by stock_expr", candidate.symbol)); + diagnostics.push(format!("{} rejected by stock_expr", symbol)); } return Ok(false); } @@ -9822,13 +9940,13 @@ impl PlatformExprStrategy { == PlatformStopTakeReferencePriceMode::SignalDayPostAdjustedClose && ctx .data - .market_latest_back_adjusted_close(date, &candidate.symbol) + .market_latest_back_adjusted_close(date, symbol) .is_none() { if diagnostics.len() < 12 { diagnostics.push(format!( "{} rejected by missing signal-day post-adjusted close", - candidate.symbol + symbol )); } return Ok(false); @@ -9968,13 +10086,14 @@ impl PlatformExprStrategy { // Selection policy is evaluated on the signal day. Lagged execution only // defers buy/sell risk to the actual execution bar; it must not disable an // explicitly configured signal-day universe filter. - let (universe, risk_decisions) = self.selection_universe_and_risk_decisions_with_options( - ctx, - date, - universe_factor_date, - SelectionRiskDeferral::None, - true, - ); + let (universe_symbol_ids, risk_decisions) = self + .selection_symbol_ids_and_risk_decisions_with_options( + ctx, + date, + universe_factor_date, + SelectionRiskDeferral::None, + true, + ); let mut diagnostics = Self::selection_risk_decision_diagnostics( &risk_decisions, date, @@ -9982,6 +10101,7 @@ impl PlatformExprStrategy { 5, ); 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); // The universe is already stably ordered by market cap. When the @@ -9989,17 +10109,26 @@ impl PlatformExprStrategy { // complete ranking for replacement limiting, select directly from the // ordered stream instead of materializing a second candidate vector. if self.rank_reuses_market_cap_order() && self.config.daily_replacement_limit == 0 { - let mut selected = Vec::with_capacity(limit.min(universe.len())); - for candidate in universe { - let stock = self.selection_stock_state_with_factor_date_from_views( + let mut selected = Vec::with_capacity(limit.min(universe_symbol_ids.len())); + for symbol_id in universe_symbol_ids { + let factor = universe_factor_day + .factor(symbol_id) + .expect("market-cap order references missing factor row"); + let symbol = factor.symbol.as_str(); + let stock = self.selection_stock_state_with_factor_date_from_views_by_symbol_id( ctx, date, stock_factor_date, - &candidate.symbol, + symbol_id, + symbol, &execution_day, &factor_day, )?; - let field_value = self.selection_field_value(&candidate, &stock); + let field_value = self.selection_field_value_from_caps( + decision_market_cap_bn(factor), + decision_free_float_cap_bn(factor), + &stock, + ); if !field_value.is_finite() || field_value < band_low || field_value > band_high { continue; } @@ -10007,11 +10136,11 @@ impl PlatformExprStrategy { ctx, date, day, - &candidate, + symbol, &stock, &mut diagnostics, )? { - selected.push(candidate.symbol); + selected.push(factor.symbol.clone()); if selected.len() >= limit { break; } @@ -10020,6 +10149,20 @@ impl PlatformExprStrategy { return Ok((selected, diagnostics, risk_decisions)); } + let universe = universe_symbol_ids + .into_iter() + .map(|symbol_id| { + let factor = universe_factor_day + .factor(symbol_id) + .expect("market-cap order references missing factor row"); + EligibleUniverseSnapshot { + symbol: factor.symbol.clone(), + market_cap_bn: decision_market_cap_bn(factor), + free_float_cap_bn: decision_free_float_cap_bn(factor), + } + }) + .collect::>(); + let mut candidates = Vec::new(); let mut missing_rank_count = 0usize; let mut missing_rank_examples = Vec::new(); @@ -10114,7 +10257,7 @@ impl PlatformExprStrategy { ctx, date, day, - &candidate, + &candidate.symbol, &stock, &mut diagnostics, )? { @@ -13920,6 +14063,16 @@ mod tests { .selection_risk_decisions(&ctx, date, date) .is_empty() ); + let (default_symbol_ids, default_direct_decisions) = default_strategy + .selection_symbol_ids_and_risk_decisions_with_options( + &ctx, + date, + date, + SelectionRiskDeferral::None, + true, + ); + assert_eq!(default_symbol_ids, vec![data.symbol_id(symbol).unwrap()]); + assert!(default_direct_decisions.is_empty()); let mut selection_cfg = PlatformExprStrategyConfig::microcap_rotation(); selection_cfg.risk_config.static_rules.reject_kcb_selection = true; let selection_strategy = PlatformExprStrategy::new(selection_cfg); @@ -13937,6 +14090,20 @@ mod tests { ); assert!(risk_diagnostics.is_empty(), "{risk_diagnostics:?}"); let risk_decisions = selection_strategy.selection_risk_decisions(&ctx, date, date); + let (rejected_symbol_ids, direct_risk_decisions) = selection_strategy + .selection_symbol_ids_and_risk_decisions_with_options( + &ctx, + date, + date, + SelectionRiskDeferral::None, + true, + ); + assert!(rejected_symbol_ids.is_empty()); + assert_eq!(direct_risk_decisions.len(), risk_decisions.len()); + assert_eq!( + direct_risk_decisions[0].rule_code, + risk_decisions[0].rule_code + ); let risk_diagnostics = PlatformExprStrategy::selection_risk_decision_diagnostics( &risk_decisions, date,