perf: specialize stock snapshot sources

This commit is contained in:
boris
2026-09-05 03:32:38 +08:00
parent 224adf70d4
commit d79678d850
+159 -42
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, CandidateEligibility, DailyFactorSnapshot, DailyMarketSnapshot, DailySnapshotView, DataSet,
decision_market_cap_bn, EligibleUniverseSnapshot, PriceField, 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;
@@ -723,6 +723,71 @@ struct StockExpressionState {
extra_text_factors: BTreeMap<String, String>, extra_text_factors: BTreeMap<String, String>,
} }
trait StockStateSnapshotSource<'a> {
fn execution_market(&self, symbol_id: u32) -> Option<&'a DailyMarketSnapshot>;
fn execution_candidate(&self, symbol_id: u32) -> Option<&'a CandidateEligibility>;
fn feature_market(&self, symbol_id: u32) -> Option<&'a DailyMarketSnapshot>;
fn factor(&self, symbol_id: u32) -> Option<&'a DailyFactorSnapshot>;
}
struct IndexedStockStateSnapshotSource<'a> {
data: &'a DataSet,
date: NaiveDate,
factor_date: NaiveDate,
}
impl<'a> StockStateSnapshotSource<'a> for IndexedStockStateSnapshotSource<'a> {
fn execution_market(&self, symbol_id: u32) -> Option<&'a DailyMarketSnapshot> {
self.data.market_by_symbol_id(self.date, symbol_id)
}
fn execution_candidate(&self, symbol_id: u32) -> Option<&'a CandidateEligibility> {
self.data.candidate_by_symbol_id(self.date, symbol_id)
}
fn feature_market(&self, symbol_id: u32) -> Option<&'a DailyMarketSnapshot> {
if self.factor_date == self.date {
return self.execution_market(symbol_id);
}
self.data
.market_by_symbol_id(self.factor_date, symbol_id)
.or_else(|| self.execution_market(symbol_id))
}
fn factor(&self, symbol_id: u32) -> Option<&'a DailyFactorSnapshot> {
self.data.factor_by_symbol_id(self.factor_date, symbol_id)
}
}
struct ViewStockStateSnapshotSource<'a, 'view> {
execution: &'view DailySnapshotView<'a>,
factor: &'view DailySnapshotView<'a>,
same_date: bool,
}
impl<'a> StockStateSnapshotSource<'a> for ViewStockStateSnapshotSource<'a, '_> {
fn execution_market(&self, symbol_id: u32) -> Option<&'a DailyMarketSnapshot> {
self.execution.market(symbol_id)
}
fn execution_candidate(&self, symbol_id: u32) -> Option<&'a CandidateEligibility> {
self.execution.candidate(symbol_id)
}
fn feature_market(&self, symbol_id: u32) -> Option<&'a DailyMarketSnapshot> {
if self.same_date {
return self.execution_market(symbol_id);
}
self.factor
.market(symbol_id)
.or_else(|| self.execution_market(symbol_id))
}
fn factor(&self, symbol_id: u32) -> Option<&'a DailyFactorSnapshot> {
self.factor.factor(symbol_id)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StockFilterQuoteUsage { enum StockFilterQuoteUsage {
DailyOnly, DailyOnly,
@@ -3897,6 +3962,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<'_>,
@@ -3915,6 +3981,32 @@ impl PlatformExprStrategy {
) )
} }
fn selection_stock_state_with_factor_date_from_views<'a>(
&self,
ctx: &StrategyContext<'a>,
date: NaiveDate,
factor_date: NaiveDate,
symbol: &str,
execution_day: &DailySnapshotView<'a>,
factor_day: &DailySnapshotView<'a>,
) -> Result<Arc<StockExpressionState>, 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(
ctx,
date,
factor_date,
symbol,
None,
use_intraday_quote,
&source,
)
}
fn stock_decision_rolling_mean( fn stock_decision_rolling_mean(
&self, &self,
ctx: &StrategyContext<'_>, ctx: &StrategyContext<'_>,
@@ -4020,6 +4112,36 @@ impl PlatformExprStrategy {
execution_time: Option<NaiveTime>, execution_time: Option<NaiveTime>,
use_intraday_quote: bool, use_intraday_quote: bool,
) -> Result<Arc<StockExpressionState>, BacktestError> { ) -> Result<Arc<StockExpressionState>, BacktestError> {
let source = IndexedStockStateSnapshotSource {
data: ctx.data,
date,
factor_date,
};
self.stock_state_with_factor_date_and_time_from_source(
ctx,
date,
factor_date,
symbol,
execution_time,
use_intraday_quote,
&source,
)
}
#[allow(clippy::too_many_arguments)]
fn stock_state_with_factor_date_and_time_from_source<'a, S>(
&self,
ctx: &StrategyContext<'a>,
date: NaiveDate,
factor_date: NaiveDate,
symbol: &str,
execution_time: Option<NaiveTime>,
use_intraday_quote: bool,
source: &S,
) -> Result<Arc<StockExpressionState>, BacktestError>
where
S: StockStateSnapshotSource<'a>,
{
let symbol_id = ctx.data.symbol_id(symbol).ok_or_else(|| { let symbol_id = ctx.data.symbol_id(symbol).ok_or_else(|| {
BacktestError::Data(crate::data::DataSetError::MissingSnapshot { BacktestError::Data(crate::data::DataSetError::MissingSnapshot {
kind: "symbol_index", kind: "symbol_index",
@@ -4048,43 +4170,28 @@ impl PlatformExprStrategy {
return Ok(Arc::clone(state)); return Ok(Arc::clone(state));
} }
let market = ctx let market = source.execution_market(symbol_id).ok_or_else(|| {
.data BacktestError::Data(crate::data::DataSetError::MissingSnapshot {
.market_by_symbol_id(date, symbol_id) kind: "market",
.ok_or_else(|| { date,
BacktestError::Data(crate::data::DataSetError::MissingSnapshot { symbol: symbol.to_string(),
kind: "market", })
date, })?;
symbol: symbol.to_string(), let candidate = source.execution_candidate(symbol_id).ok_or_else(|| {
}) BacktestError::Data(crate::data::DataSetError::MissingSnapshot {
})?; kind: "candidate",
let candidate = ctx date,
.data symbol: symbol.to_string(),
.candidate_by_symbol_id(date, symbol_id) })
.ok_or_else(|| { })?;
BacktestError::Data(crate::data::DataSetError::MissingSnapshot { let feature_market = source.feature_market(symbol_id).unwrap_or(market);
kind: "candidate", let factor = source.factor(symbol_id).ok_or_else(|| {
date, BacktestError::Data(crate::data::DataSetError::MissingSnapshot {
symbol: symbol.to_string(), kind: "factor",
}) date: factor_date,
})?; symbol: symbol.to_string(),
let feature_market = if factor_date == date { })
market })?;
} 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)
.ok_or_else(|| {
BacktestError::Data(crate::data::DataSetError::MissingSnapshot {
kind: "factor",
date: factor_date,
symbol: symbol.to_string(),
})
})?;
let intraday_same_day_factor = self.uses_intraday_execution_quotes() let intraday_same_day_factor = self.uses_intraday_execution_quotes()
&& factor_date == date && factor_date == date
&& !ctx.is_lagged_execution(); && !ctx.is_lagged_execution();
@@ -9874,6 +9981,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
@@ -9882,11 +9991,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.symbol,
&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 {
@@ -9913,11 +10024,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.symbol,
&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() {
@@ -10635,13 +10748,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.symbol,
&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() {