perf: reuse daily snapshot views in stock selection

This commit is contained in:
boris
2026-09-05 00:14:57 +08:00
parent db155e7ad0
commit 1df0081479
2 changed files with 138 additions and 36 deletions
+52 -17
View File
@@ -522,6 +522,7 @@ pub struct YieldCurvePoint {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct EligibleUniverseSnapshot { pub struct EligibleUniverseSnapshot {
pub symbol: String, pub symbol: String,
pub symbol_id: u32,
pub market_cap_bn: f64, pub market_cap_bn: f64,
pub free_float_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. /// the already indexed slices once and keeps all lookups read-only.
pub(crate) struct DailySnapshotView<'a> { pub(crate) struct DailySnapshotView<'a> {
market: DailySymbolRows<'a, DailyMarketSnapshot>, market: DailySymbolRows<'a, DailyMarketSnapshot>,
factor_rows: &'a [DailyFactorSnapshot], factors: DailySymbolRows<'a, DailyFactorSnapshot>,
factor_symbol_ids: &'a [u32],
candidates: DailySymbolRows<'a, CandidateEligibility>, candidates: DailySymbolRows<'a, CandidateEligibility>,
} }
@@ -1371,12 +1371,16 @@ impl<'a> DailySnapshotView<'a> {
self.candidates.get(symbol_id) 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] { pub(crate) fn factor_rows(&self) -> &'a [DailyFactorSnapshot] {
self.factor_rows self.factors.rows
} }
pub(crate) fn factor_symbol_ids(&self) -> &'a [u32] { 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_symbol_ids_by_date,
&self.market_row_positions_by_date, &self.market_row_positions_by_date,
), ),
factor_rows: self factors: rows_on(
.factor_by_date date,
.get(&date) &self.factor_by_date,
.map(Vec::as_slice) &self.factor_symbol_ids_by_date,
.unwrap_or(&[]), &self.factor_row_positions_by_date,
factor_symbol_ids: self ),
.factor_symbol_ids_by_date
.get(&date)
.map(Vec::as_slice)
.unwrap_or(&[]),
candidates: rows_on( candidates: rows_on(
date, date,
&self.candidate_by_date, &self.candidate_by_date,
@@ -3650,14 +3650,25 @@ impl DataSet {
pub fn eligible_universe_on(&self, date: NaiveDate) -> &[EligibleUniverseSnapshot] { pub fn eligible_universe_on(&self, date: NaiveDate) -> &[EligibleUniverseSnapshot] {
self.eligible_universe_by_date 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) .get(&date)
.map(Vec::as_slice) .map(Vec::as_slice)
.unwrap_or(&[]) .unwrap_or(&[])
} }
pub fn fundamental_universe_on(&self, date: NaiveDate) -> Vec<EligibleUniverseSnapshot> { pub fn fundamental_universe_on(&self, date: NaiveDate) -> Vec<EligibleUniverseSnapshot> {
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( pub fn eligible_universe_on_with_risk_config(
@@ -3671,6 +3682,7 @@ impl DataSet {
&self.candidate_by_date, &self.candidate_by_date,
&self.market_by_date, &self.market_by_date,
&self.instruments, &self.instruments,
&self.symbol_id_by_code,
risk_config, risk_config,
) )
} }
@@ -4450,11 +4462,17 @@ fn build_order_book_depth_index(
fn build_eligible_universe( fn build_eligible_universe(
factor_by_date: &BTreeMap<NaiveDate, Vec<DailyFactorSnapshot>>, factor_by_date: &BTreeMap<NaiveDate, Vec<DailyFactorSnapshot>>,
market_by_date: &BTreeMap<NaiveDate, Vec<DailyMarketSnapshot>>, market_by_date: &BTreeMap<NaiveDate, Vec<DailyMarketSnapshot>>,
symbol_id_by_code: &AHashMap<String, u32>,
) -> BTreeMap<NaiveDate, Vec<EligibleUniverseSnapshot>> { ) -> BTreeMap<NaiveDate, Vec<EligibleUniverseSnapshot>> {
let mut per_date = BTreeMap::<NaiveDate, Vec<EligibleUniverseSnapshot>>::new(); let mut per_date = BTreeMap::<NaiveDate, Vec<EligibleUniverseSnapshot>>::new();
for date in factor_by_date.keys() { 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); per_date.insert(*date, rows);
} }
@@ -4465,12 +4483,16 @@ fn build_fundamental_universe_for_date(
date: NaiveDate, date: NaiveDate,
factor_by_date: &BTreeMap<NaiveDate, Vec<DailyFactorSnapshot>>, factor_by_date: &BTreeMap<NaiveDate, Vec<DailyFactorSnapshot>>,
market_by_date: &BTreeMap<NaiveDate, Vec<DailyMarketSnapshot>>, market_by_date: &BTreeMap<NaiveDate, Vec<DailyMarketSnapshot>>,
symbol_id_by_code: &AHashMap<String, u32>,
) -> Vec<EligibleUniverseSnapshot> { ) -> Vec<EligibleUniverseSnapshot> {
let mut rows = Vec::new(); let mut rows = Vec::new();
let Some(factors) = factor_by_date.get(&date) else { let Some(factors) = factor_by_date.get(&date) else {
return rows; return rows;
}; };
for factor in factors { for factor in factors {
let Some(symbol_id) = symbol_id_by_code.get(&factor.symbol).copied() else {
continue;
};
if market_by_date if market_by_date
.get(&date) .get(&date)
.and_then(|rows| find_by_symbol(rows, &factor.symbol, |row| row.symbol.as_str())) .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 { rows.push(EligibleUniverseSnapshot {
symbol: factor.symbol.clone(), symbol: factor.symbol.clone(),
symbol_id,
market_cap_bn, market_cap_bn,
free_float_cap_bn: decision_free_float_cap_bn(factor), free_float_cap_bn: decision_free_float_cap_bn(factor),
}); });
@@ -4503,6 +4526,7 @@ fn build_eligible_universe_for_date(
candidate_by_date: &BTreeMap<NaiveDate, Vec<CandidateEligibility>>, candidate_by_date: &BTreeMap<NaiveDate, Vec<CandidateEligibility>>,
market_by_date: &BTreeMap<NaiveDate, Vec<DailyMarketSnapshot>>, market_by_date: &BTreeMap<NaiveDate, Vec<DailyMarketSnapshot>>,
instruments: &HashMap<String, Instrument>, instruments: &HashMap<String, Instrument>,
symbol_id_by_code: &AHashMap<String, u32>,
risk_config: &FidcRiskControlConfig, risk_config: &FidcRiskControlConfig,
) -> Vec<EligibleUniverseSnapshot> { ) -> Vec<EligibleUniverseSnapshot> {
factor_by_date factor_by_date
@@ -4514,6 +4538,7 @@ fn build_eligible_universe_for_date(
candidate_by_date, candidate_by_date,
market_by_date, market_by_date,
instruments, instruments,
symbol_id_by_code,
risk_config, risk_config,
) )
}) })
@@ -4526,10 +4551,14 @@ fn build_eligible_universe_for_date_from_factors(
candidate_by_date: &BTreeMap<NaiveDate, Vec<CandidateEligibility>>, candidate_by_date: &BTreeMap<NaiveDate, Vec<CandidateEligibility>>,
market_by_date: &BTreeMap<NaiveDate, Vec<DailyMarketSnapshot>>, market_by_date: &BTreeMap<NaiveDate, Vec<DailyMarketSnapshot>>,
instruments: &HashMap<String, Instrument>, instruments: &HashMap<String, Instrument>,
symbol_id_by_code: &AHashMap<String, u32>,
risk_config: &FidcRiskControlConfig, risk_config: &FidcRiskControlConfig,
) -> Vec<EligibleUniverseSnapshot> { ) -> Vec<EligibleUniverseSnapshot> {
let mut rows = Vec::new(); let mut rows = Vec::new();
for factor in factors { 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() { if factor.market_cap_bn <= 0.0 || !factor.market_cap_bn.is_finite() {
continue; continue;
} }
@@ -4567,6 +4596,7 @@ fn build_eligible_universe_for_date_from_factors(
let free_float_cap_bn = decision_free_float_cap_bn(factor); let free_float_cap_bn = decision_free_float_cap_bn(factor);
rows.push(EligibleUniverseSnapshot { rows.push(EligibleUniverseSnapshot {
symbol: factor.symbol.clone(), symbol: factor.symbol.clone(),
symbol_id,
market_cap_bn, market_cap_bn,
free_float_cap_bn, free_float_cap_bn,
}); });
@@ -5005,6 +5035,10 @@ mod tests {
.map(|row| row.symbol.as_str()), .map(|row| row.symbol.as_str()),
Some(symbol) Some(symbol)
); );
assert_eq!(
day.factor(symbol_id).map(|row| row.symbol.as_str()),
Some(symbol)
);
assert_eq!( assert_eq!(
data.candidate_by_symbol_id(date, symbol_id) data.candidate_by_symbol_id(date, symbol_id)
.map(|row| row.symbol.as_str()), .map(|row| row.symbol.as_str()),
@@ -5024,6 +5058,7 @@ mod tests {
Some("000300.SH") Some("000300.SH")
); );
assert!(data.factor_by_symbol_id(date, signal_id).is_none()); 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!(data.candidate_by_symbol_id(date, signal_id).is_none());
assert!(day.candidate(signal_id).is_none()); assert!(day.candidate(signal_id).is_none());
assert_eq!( assert_eq!(
+86 -19
View File
@@ -9,8 +9,8 @@ use rhai::{AST, Dynamic, Engine, Map, Scope};
use crate::broker::{MatchingType, RebalanceCashMode, SlippageModel}; use crate::broker::{MatchingType, RebalanceCashMode, SlippageModel};
use crate::cost::ChinaAShareCostModel; use crate::cost::ChinaAShareCostModel;
use crate::data::{ use crate::data::{
DailyMarketSnapshot, EligibleUniverseSnapshot, PriceField, decision_free_float_cap_bn, DailyMarketSnapshot, DailySnapshotView, EligibleUniverseSnapshot, PriceField,
decision_market_cap_bn, decision_free_float_cap_bn, decision_market_cap_bn,
}; };
use crate::engine::BacktestError; use crate::engine::BacktestError;
use crate::events::OrderSide; 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) self.stock_state_with_factor_date_and_time(ctx, date, factor_date, symbol, None, true)
} }
#[cfg(test)]
fn selection_stock_state_with_factor_date( fn selection_stock_state_with_factor_date(
&self, &self,
ctx: &StrategyContext<'_>, 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<Arc<StockExpressionState>, 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( fn stock_decision_rolling_mean(
&self, &self,
ctx: &StrategyContext<'_>, ctx: &StrategyContext<'_>,
@@ -4013,6 +4037,32 @@ impl PlatformExprStrategy {
symbol: symbol.to_string(), 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<NaiveTime>,
use_intraday_quote: bool,
execution_day: Option<&DailySnapshotView<'_>>,
factor_day: Option<&DailySnapshotView<'_>>,
) -> Result<Arc<StockExpressionState>, BacktestError> {
let calendar_index = { let calendar_index = {
let mut cache_date = self.stock_state_cache_date.borrow_mut(); let mut cache_date = self.stock_state_cache_date.borrow_mut();
if *cache_date != Some(date) { if *cache_date != Some(date) {
@@ -4033,9 +4083,9 @@ impl PlatformExprStrategy {
return Ok(Arc::clone(state)); return Ok(Arc::clone(state));
} }
let market = ctx let market = execution_day
.data .and_then(|view| view.market(symbol_id))
.market_by_symbol_id(date, symbol_id) .or_else(|| ctx.data.market_by_symbol_id(date, symbol_id))
.ok_or_else(|| { .ok_or_else(|| {
BacktestError::Data(crate::data::DataSetError::MissingSnapshot { BacktestError::Data(crate::data::DataSetError::MissingSnapshot {
kind: "market", kind: "market",
@@ -4043,9 +4093,9 @@ impl PlatformExprStrategy {
symbol: symbol.to_string(), symbol: symbol.to_string(),
}) })
})?; })?;
let candidate = ctx let candidate = execution_day
.data .and_then(|view| view.candidate(symbol_id))
.candidate_by_symbol_id(date, symbol_id) .or_else(|| ctx.data.candidate_by_symbol_id(date, symbol_id))
.ok_or_else(|| { .ok_or_else(|| {
BacktestError::Data(crate::data::DataSetError::MissingSnapshot { BacktestError::Data(crate::data::DataSetError::MissingSnapshot {
kind: "candidate", kind: "candidate",
@@ -4056,13 +4106,14 @@ impl PlatformExprStrategy {
let feature_market = if factor_date == date { let feature_market = if factor_date == date {
market market
} else { } else {
ctx.data factor_day
.market_by_symbol_id(factor_date, symbol_id) .and_then(|view| view.market(symbol_id))
.or_else(|| ctx.data.market_by_symbol_id(factor_date, symbol_id))
.unwrap_or(market) .unwrap_or(market)
}; };
let factor = ctx let factor = factor_day
.data .and_then(|view| view.factor(symbol_id))
.factor_by_symbol_id(factor_date, symbol_id) .or_else(|| ctx.data.factor_by_symbol_id(factor_date, symbol_id))
.ok_or_else(|| { .ok_or_else(|| {
BacktestError::Data(crate::data::DataSetError::MissingSnapshot { BacktestError::Data(crate::data::DataSetError::MissingSnapshot {
kind: "factor", kind: "factor",
@@ -9315,6 +9366,7 @@ impl PlatformExprStrategy {
let free_float_cap_bn = decision_free_float_cap_bn(factor); let free_float_cap_bn = decision_free_float_cap_bn(factor);
rows.push(EligibleUniverseSnapshot { rows.push(EligibleUniverseSnapshot {
symbol: factor.symbol.clone(), symbol: factor.symbol.clone(),
symbol_id,
market_cap_bn, market_cap_bn,
free_float_cap_bn, free_float_cap_bn,
}); });
@@ -9793,6 +9845,8 @@ impl PlatformExprStrategy {
universe_factor_date, universe_factor_date,
5, 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 // The universe is already stably ordered by market cap. When the
// strategy asks for that exact ascending order and does not need a // 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 { if self.rank_reuses_market_cap_order() && self.config.daily_replacement_limit == 0 {
let mut selected = Vec::with_capacity(limit.min(universe.len())); let mut selected = Vec::with_capacity(limit.min(universe.len()));
for candidate in universe { 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, ctx,
date, date,
stock_factor_date, stock_factor_date,
&candidate.symbol, &candidate,
&execution_day,
&factor_day,
)?; )?;
let field_value = self.selection_field_value(&candidate, &stock); let field_value = self.selection_field_value(&candidate, &stock);
if !field_value.is_finite() || field_value < band_low || field_value > band_high { 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_count = 0usize;
let mut missing_rank_examples = Vec::new(); let mut missing_rank_examples = Vec::new();
for candidate in universe { 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, ctx,
date, date,
stock_factor_date, stock_factor_date,
&candidate.symbol, &candidate,
&execution_day,
&factor_day,
)?; )?;
let field_value = self.selection_field_value(&candidate, &stock); let field_value = self.selection_field_value(&candidate, &stock);
if !field_value.is_finite() { if !field_value.is_finite() {
@@ -10517,13 +10575,17 @@ impl PlatformExprStrategy {
universe_factor_date, universe_factor_date,
selection_risk_deferral, 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); let quote_candidate_limit = self.quote_plan_candidate_limit(selection_limit);
for candidate in universe { 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, ctx,
date, date,
stock_factor_date, stock_factor_date,
&candidate.symbol, &candidate,
&execution_day,
&factor_day,
)?; )?;
let field_value = self.selection_field_value(&candidate, &stock); let field_value = self.selection_field_value(&candidate, &stock);
if !field_value.is_finite() { if !field_value.is_finite() {
@@ -18395,11 +18457,16 @@ mod tests {
.expect("bjse state"); .expect("bjse state");
let star_candidate = EligibleUniverseSnapshot { let star_candidate = EligibleUniverseSnapshot {
symbol: star_st_symbol.to_string(), 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, market_cap_bn: 1.0,
free_float_cap_bn: 1.0, free_float_cap_bn: 1.0,
}; };
let bjse_candidate = EligibleUniverseSnapshot { let bjse_candidate = EligibleUniverseSnapshot {
symbol: bjse_symbol.to_string(), symbol: bjse_symbol.to_string(),
symbol_id: ctx.data.symbol_id(bjse_symbol).expect("bjse symbol id"),
market_cap_bn: 2.0, market_cap_bn: 2.0,
free_float_cap_bn: 2.0, free_float_cap_bn: 2.0,
}; };