From 1df0081479b93a051f9a40d2acf7060d0eb6929d Mon Sep 17 00:00:00 2001 From: boris Date: Sat, 5 Sep 2026 00:14:57 +0800 Subject: [PATCH] perf: reuse daily snapshot views in stock selection --- crates/fidc-core/src/data.rs | 69 +++++++++--- .../fidc-core/src/platform_expr_strategy.rs | 105 ++++++++++++++---- 2 files changed, 138 insertions(+), 36 deletions(-) diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index b977138..8dca400 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -522,6 +522,7 @@ pub struct YieldCurvePoint { #[derive(Debug, Clone)] pub struct EligibleUniverseSnapshot { pub symbol: String, + pub symbol_id: u32, pub market_cap_bn: f64, pub free_float_cap_bn: f64, } @@ -1357,8 +1358,7 @@ impl<'a, T> DailySymbolRows<'a, T> { /// the already indexed slices once and keeps all lookups read-only. pub(crate) struct DailySnapshotView<'a> { market: DailySymbolRows<'a, DailyMarketSnapshot>, - factor_rows: &'a [DailyFactorSnapshot], - factor_symbol_ids: &'a [u32], + factors: DailySymbolRows<'a, DailyFactorSnapshot>, candidates: DailySymbolRows<'a, CandidateEligibility>, } @@ -1371,12 +1371,16 @@ impl<'a> DailySnapshotView<'a> { self.candidates.get(symbol_id) } + pub(crate) fn factor(&self, symbol_id: u32) -> Option<&'a DailyFactorSnapshot> { + self.factors.get(symbol_id) + } + pub(crate) fn factor_rows(&self) -> &'a [DailyFactorSnapshot] { - self.factor_rows + self.factors.rows } pub(crate) fn factor_symbol_ids(&self) -> &'a [u32] { - self.factor_symbol_ids + self.factors.symbol_ids } } @@ -1950,16 +1954,12 @@ impl DataSet { &self.market_symbol_ids_by_date, &self.market_row_positions_by_date, ), - factor_rows: self - .factor_by_date - .get(&date) - .map(Vec::as_slice) - .unwrap_or(&[]), - factor_symbol_ids: self - .factor_symbol_ids_by_date - .get(&date) - .map(Vec::as_slice) - .unwrap_or(&[]), + factors: rows_on( + date, + &self.factor_by_date, + &self.factor_symbol_ids_by_date, + &self.factor_row_positions_by_date, + ), candidates: rows_on( date, &self.candidate_by_date, @@ -3650,14 +3650,25 @@ impl DataSet { pub fn eligible_universe_on(&self, date: NaiveDate) -> &[EligibleUniverseSnapshot] { self.eligible_universe_by_date - .get_or_init(|| build_eligible_universe(&self.factor_by_date, &self.market_by_date)) + .get_or_init(|| { + build_eligible_universe( + &self.factor_by_date, + &self.market_by_date, + &self.symbol_id_by_code, + ) + }) .get(&date) .map(Vec::as_slice) .unwrap_or(&[]) } pub fn fundamental_universe_on(&self, date: NaiveDate) -> Vec { - build_fundamental_universe_for_date(date, &self.factor_by_date, &self.market_by_date) + build_fundamental_universe_for_date( + date, + &self.factor_by_date, + &self.market_by_date, + &self.symbol_id_by_code, + ) } pub fn eligible_universe_on_with_risk_config( @@ -3671,6 +3682,7 @@ impl DataSet { &self.candidate_by_date, &self.market_by_date, &self.instruments, + &self.symbol_id_by_code, risk_config, ) } @@ -4450,11 +4462,17 @@ fn build_order_book_depth_index( fn build_eligible_universe( factor_by_date: &BTreeMap>, market_by_date: &BTreeMap>, + symbol_id_by_code: &AHashMap, ) -> BTreeMap> { let mut per_date = BTreeMap::>::new(); for date in factor_by_date.keys() { - let rows = build_fundamental_universe_for_date(*date, factor_by_date, market_by_date); + let rows = build_fundamental_universe_for_date( + *date, + factor_by_date, + market_by_date, + symbol_id_by_code, + ); per_date.insert(*date, rows); } @@ -4465,12 +4483,16 @@ fn build_fundamental_universe_for_date( date: NaiveDate, factor_by_date: &BTreeMap>, market_by_date: &BTreeMap>, + symbol_id_by_code: &AHashMap, ) -> Vec { let mut rows = Vec::new(); let Some(factors) = factor_by_date.get(&date) else { return rows; }; for factor in factors { + let Some(symbol_id) = symbol_id_by_code.get(&factor.symbol).copied() else { + continue; + }; if market_by_date .get(&date) .and_then(|rows| find_by_symbol(rows, &factor.symbol, |row| row.symbol.as_str())) @@ -4484,6 +4506,7 @@ fn build_fundamental_universe_for_date( } rows.push(EligibleUniverseSnapshot { symbol: factor.symbol.clone(), + symbol_id, market_cap_bn, free_float_cap_bn: decision_free_float_cap_bn(factor), }); @@ -4503,6 +4526,7 @@ fn build_eligible_universe_for_date( candidate_by_date: &BTreeMap>, market_by_date: &BTreeMap>, instruments: &HashMap, + symbol_id_by_code: &AHashMap, risk_config: &FidcRiskControlConfig, ) -> Vec { factor_by_date @@ -4514,6 +4538,7 @@ fn build_eligible_universe_for_date( candidate_by_date, market_by_date, instruments, + symbol_id_by_code, risk_config, ) }) @@ -4526,10 +4551,14 @@ fn build_eligible_universe_for_date_from_factors( candidate_by_date: &BTreeMap>, market_by_date: &BTreeMap>, instruments: &HashMap, + symbol_id_by_code: &AHashMap, risk_config: &FidcRiskControlConfig, ) -> Vec { let mut rows = Vec::new(); for factor in factors { + let Some(symbol_id) = symbol_id_by_code.get(&factor.symbol).copied() else { + continue; + }; if factor.market_cap_bn <= 0.0 || !factor.market_cap_bn.is_finite() { continue; } @@ -4567,6 +4596,7 @@ fn build_eligible_universe_for_date_from_factors( let free_float_cap_bn = decision_free_float_cap_bn(factor); rows.push(EligibleUniverseSnapshot { symbol: factor.symbol.clone(), + symbol_id, market_cap_bn, free_float_cap_bn, }); @@ -5005,6 +5035,10 @@ mod tests { .map(|row| row.symbol.as_str()), Some(symbol) ); + assert_eq!( + day.factor(symbol_id).map(|row| row.symbol.as_str()), + Some(symbol) + ); assert_eq!( data.candidate_by_symbol_id(date, symbol_id) .map(|row| row.symbol.as_str()), @@ -5024,6 +5058,7 @@ mod tests { Some("000300.SH") ); assert!(data.factor_by_symbol_id(date, signal_id).is_none()); + assert!(day.factor(signal_id).is_none()); assert!(data.candidate_by_symbol_id(date, signal_id).is_none()); assert!(day.candidate(signal_id).is_none()); assert_eq!( diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 5d96d81..38e3063 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -9,8 +9,8 @@ use rhai::{AST, Dynamic, Engine, Map, Scope}; use crate::broker::{MatchingType, RebalanceCashMode, SlippageModel}; use crate::cost::ChinaAShareCostModel; use crate::data::{ - DailyMarketSnapshot, EligibleUniverseSnapshot, PriceField, decision_free_float_cap_bn, - decision_market_cap_bn, + DailyMarketSnapshot, DailySnapshotView, EligibleUniverseSnapshot, PriceField, + decision_free_float_cap_bn, decision_market_cap_bn, }; use crate::engine::BacktestError; use crate::events::OrderSide; @@ -3883,6 +3883,7 @@ impl PlatformExprStrategy { self.stock_state_with_factor_date_and_time(ctx, date, factor_date, symbol, None, true) } + #[cfg(test)] fn selection_stock_state_with_factor_date( &self, ctx: &StrategyContext<'_>, @@ -3901,6 +3902,29 @@ impl PlatformExprStrategy { ) } + fn selection_stock_state_with_factor_date_from_views( + &self, + ctx: &StrategyContext<'_>, + date: NaiveDate, + factor_date: NaiveDate, + candidate: &EligibleUniverseSnapshot, + execution_day: &DailySnapshotView<'_>, + factor_day: &DailySnapshotView<'_>, + ) -> Result, BacktestError> { + let use_intraday_quote = self.selection_quote_usage != StockFilterQuoteUsage::DailyOnly; + self.stock_state_with_factor_date_and_time_by_symbol_id( + ctx, + date, + factor_date, + candidate.symbol_id, + &candidate.symbol, + None, + use_intraday_quote, + Some(execution_day), + Some(factor_day), + ) + } + fn stock_decision_rolling_mean( &self, ctx: &StrategyContext<'_>, @@ -4013,6 +4037,32 @@ impl PlatformExprStrategy { symbol: symbol.to_string(), }) })?; + self.stock_state_with_factor_date_and_time_by_symbol_id( + ctx, + date, + factor_date, + symbol_id, + symbol, + execution_time, + use_intraday_quote, + None, + None, + ) + } + + #[allow(clippy::too_many_arguments)] + fn stock_state_with_factor_date_and_time_by_symbol_id( + &self, + ctx: &StrategyContext<'_>, + date: NaiveDate, + factor_date: NaiveDate, + symbol_id: u32, + symbol: &str, + execution_time: Option, + use_intraday_quote: bool, + execution_day: Option<&DailySnapshotView<'_>>, + factor_day: Option<&DailySnapshotView<'_>>, + ) -> Result, BacktestError> { let calendar_index = { let mut cache_date = self.stock_state_cache_date.borrow_mut(); if *cache_date != Some(date) { @@ -4033,9 +4083,9 @@ impl PlatformExprStrategy { return Ok(Arc::clone(state)); } - let market = ctx - .data - .market_by_symbol_id(date, symbol_id) + let market = execution_day + .and_then(|view| view.market(symbol_id)) + .or_else(|| ctx.data.market_by_symbol_id(date, symbol_id)) .ok_or_else(|| { BacktestError::Data(crate::data::DataSetError::MissingSnapshot { kind: "market", @@ -4043,9 +4093,9 @@ impl PlatformExprStrategy { symbol: symbol.to_string(), }) })?; - let candidate = ctx - .data - .candidate_by_symbol_id(date, symbol_id) + let candidate = execution_day + .and_then(|view| view.candidate(symbol_id)) + .or_else(|| ctx.data.candidate_by_symbol_id(date, symbol_id)) .ok_or_else(|| { BacktestError::Data(crate::data::DataSetError::MissingSnapshot { kind: "candidate", @@ -4056,13 +4106,14 @@ impl PlatformExprStrategy { let feature_market = if factor_date == date { market } else { - ctx.data - .market_by_symbol_id(factor_date, symbol_id) + factor_day + .and_then(|view| view.market(symbol_id)) + .or_else(|| ctx.data.market_by_symbol_id(factor_date, symbol_id)) .unwrap_or(market) }; - let factor = ctx - .data - .factor_by_symbol_id(factor_date, symbol_id) + let factor = factor_day + .and_then(|view| view.factor(symbol_id)) + .or_else(|| ctx.data.factor_by_symbol_id(factor_date, symbol_id)) .ok_or_else(|| { BacktestError::Data(crate::data::DataSetError::MissingSnapshot { kind: "factor", @@ -9315,6 +9366,7 @@ impl PlatformExprStrategy { let free_float_cap_bn = decision_free_float_cap_bn(factor); rows.push(EligibleUniverseSnapshot { symbol: factor.symbol.clone(), + symbol_id, market_cap_bn, free_float_cap_bn, }); @@ -9793,6 +9845,8 @@ impl PlatformExprStrategy { universe_factor_date, 5, ); + let execution_day = ctx.data.daily_snapshot_view(date); + let factor_day = ctx.data.daily_snapshot_view(stock_factor_date); // The universe is already stably ordered by market cap. When the // strategy asks for that exact ascending order and does not need a @@ -9801,11 +9855,13 @@ impl PlatformExprStrategy { 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( + let stock = self.selection_stock_state_with_factor_date_from_views( ctx, date, stock_factor_date, - &candidate.symbol, + &candidate, + &execution_day, + &factor_day, )?; let field_value = self.selection_field_value(&candidate, &stock); if !field_value.is_finite() || field_value < band_low || field_value > band_high { @@ -9832,11 +9888,13 @@ impl PlatformExprStrategy { let mut missing_rank_count = 0usize; let mut missing_rank_examples = Vec::new(); for candidate in universe { - let stock = self.selection_stock_state_with_factor_date( + let stock = self.selection_stock_state_with_factor_date_from_views( ctx, date, stock_factor_date, - &candidate.symbol, + &candidate, + &execution_day, + &factor_day, )?; let field_value = self.selection_field_value(&candidate, &stock); if !field_value.is_finite() { @@ -10517,13 +10575,17 @@ impl PlatformExprStrategy { universe_factor_date, selection_risk_deferral, ); + let execution_day = ctx.data.daily_snapshot_view(date); + let factor_day = ctx.data.daily_snapshot_view(stock_factor_date); let quote_candidate_limit = self.quote_plan_candidate_limit(selection_limit); for candidate in universe { - let stock = self.selection_stock_state_with_factor_date( + let stock = self.selection_stock_state_with_factor_date_from_views( ctx, date, stock_factor_date, - &candidate.symbol, + &candidate, + &execution_day, + &factor_day, )?; let field_value = self.selection_field_value(&candidate, &stock); if !field_value.is_finite() { @@ -18395,11 +18457,16 @@ mod tests { .expect("bjse state"); let star_candidate = EligibleUniverseSnapshot { symbol: star_st_symbol.to_string(), + symbol_id: ctx + .data + .symbol_id(star_st_symbol) + .expect("star st symbol id"), market_cap_bn: 1.0, free_float_cap_bn: 1.0, }; let bjse_candidate = EligibleUniverseSnapshot { symbol: bjse_symbol.to_string(), + symbol_id: ctx.data.symbol_id(bjse_symbol).expect("bjse symbol id"), market_cap_bn: 2.0, free_float_cap_bn: 2.0, };