diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 6a41155..cebd83b 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, + CandidateEligibility, DailyFactorSnapshot, DailyMarketSnapshot, DailySnapshotView, DataSet, + EligibleUniverseSnapshot, PriceField, decision_free_float_cap_bn, decision_market_cap_bn, }; use crate::engine::BacktestError; use crate::events::OrderSide; @@ -723,6 +723,71 @@ struct StockExpressionState { extra_text_factors: BTreeMap, } +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)] enum StockFilterQuoteUsage { DailyOnly, @@ -3897,6 +3962,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<'_>, @@ -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, 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( &self, ctx: &StrategyContext<'_>, @@ -4020,6 +4112,36 @@ impl PlatformExprStrategy { execution_time: Option, use_intraday_quote: bool, ) -> Result, 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, + use_intraday_quote: bool, + source: &S, + ) -> Result, BacktestError> + where + S: StockStateSnapshotSource<'a>, + { let symbol_id = ctx.data.symbol_id(symbol).ok_or_else(|| { BacktestError::Data(crate::data::DataSetError::MissingSnapshot { kind: "symbol_index", @@ -4048,43 +4170,28 @@ impl PlatformExprStrategy { return Ok(Arc::clone(state)); } - let market = ctx - .data - .market_by_symbol_id(date, symbol_id) - .ok_or_else(|| { - BacktestError::Data(crate::data::DataSetError::MissingSnapshot { - kind: "market", - date, - symbol: symbol.to_string(), - }) - })?; - let candidate = ctx - .data - .candidate_by_symbol_id(date, symbol_id) - .ok_or_else(|| { - BacktestError::Data(crate::data::DataSetError::MissingSnapshot { - kind: "candidate", - 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 market = source.execution_market(symbol_id).ok_or_else(|| { + BacktestError::Data(crate::data::DataSetError::MissingSnapshot { + 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", + date, + symbol: symbol.to_string(), + }) + })?; + let feature_market = source.feature_market(symbol_id).unwrap_or(market); + let factor = source.factor(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() && factor_date == date && !ctx.is_lagged_execution(); @@ -9874,6 +9981,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 @@ -9882,11 +9991,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, + &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 { @@ -9913,11 +10024,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, + &execution_day, + &factor_day, )?; let field_value = self.selection_field_value(&candidate, &stock); if !field_value.is_finite() { @@ -10635,13 +10748,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, + &execution_day, + &factor_day, )?; let field_value = self.selection_field_value(&candidate, &stock); if !field_value.is_finite() {