diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 25f3a87..282fe76 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -3981,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, + ) + } + #[allow(clippy::too_many_arguments)] fn uncached_selection_stock_state_from_views_by_symbol_id<'a>( &self, @@ -9471,7 +9497,6 @@ impl PlatformExprStrategy { ) } - #[cfg(test)] fn selectable_universe_on_with_options( &self, ctx: &StrategyContext<'_>, @@ -9489,7 +9514,6 @@ impl PlatformExprStrategy { .0 } - #[cfg(test)] fn selection_universe_and_risk_decisions_with_options( &self, ctx: &StrategyContext<'_>, @@ -10117,14 +10141,15 @@ impl PlatformExprStrategy { universe_factor_date, 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 // strategy asks for that exact ascending order and does not need a // 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 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); let mut selected = Vec::with_capacity(limit.min(universe_symbol_ids.len())); for symbol_id in universe_symbol_ids { let factor = universe_factor_day @@ -10165,57 +10190,28 @@ impl PlatformExprStrategy { return Ok((selected, diagnostics, risk_decisions)); } - let selected = self.select_ranked_symbols( - ctx, - date, - universe_factor_date, - stock_factor_date, - day, - band_low, - band_high, - limit, - universe_symbol_ids, - &mut diagnostics, - )?; - 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::>(); - #[inline(never)] - #[allow(clippy::too_many_arguments)] - fn select_ranked_symbols( - &self, - ctx: &StrategyContext<'_>, - date: NaiveDate, - universe_factor_date: NaiveDate, - stock_factor_date: NaiveDate, - day: &DayExpressionState, - band_low: f64, - band_high: f64, - limit: usize, - universe_symbol_ids: Vec, - diagnostics: &mut Vec, - ) -> Result, BacktestError> { - 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); - let mut candidates = Vec::with_capacity(universe_symbol_ids.len()); - let mut candidate_states = Vec::with_capacity(universe_symbol_ids.len()); + let mut candidates = Vec::new(); let mut missing_rank_count = 0usize; let mut missing_rank_examples = Vec::new(); - for symbol_id in universe_symbol_ids { - let factor = universe_factor_day - .factor(symbol_id) - .expect("market-cap order references missing factor row"); - let candidate = EligibleUniverseSnapshot { - symbol: factor.symbol.clone(), - market_cap_bn: decision_market_cap_bn(factor), - free_float_cap_bn: decision_free_float_cap_bn(factor), - }; - let stock = self.uncached_selection_stock_state_from_views_by_symbol_id( + for candidate in universe { + let stock = self.selection_stock_state_with_factor_date_from_views( ctx, date, stock_factor_date, - symbol_id, &candidate.symbol, &execution_day, &factor_day, @@ -10273,14 +10269,12 @@ impl PlatformExprStrategy { } continue; } - let state_index = candidate_states.len(); - candidate_states.push(stock); - candidates.push((candidate, rank_value, state_index)); + candidates.push((candidate, stock, rank_value)); } if !self.rank_reuses_market_cap_order() { candidates.sort_by(|lhs, rhs| { - let lhs_value = lhs.1; - let rhs_value = rhs.1; + let lhs_value = lhs.2; + let rhs_value = rhs.2; let ordering = if self.config.rank_desc { rhs_value .partial_cmp(&lhs_value) @@ -10299,15 +10293,14 @@ impl PlatformExprStrategy { } let mut selected = Vec::new(); - for (candidate, _, state_index) in candidates { - let stock = &candidate_states[state_index]; + for (candidate, stock, _) in candidates { if !self.selection_candidate_passes_filters( ctx, date, day, &candidate.symbol, - stock, - diagnostics, + &stock, + &mut diagnostics, )? { continue; } @@ -10324,7 +10317,7 @@ impl PlatformExprStrategy { )); } - Ok(selected) + Ok((selected, diagnostics, risk_decisions)) } fn stock_filter_quote_usage(&self) -> StockFilterQuoteUsage { @@ -10920,6 +10913,7 @@ impl PlatformExprStrategy { selection_limit: usize, ) -> Result<(Vec, Vec, usize, Vec), BacktestError> { let mut diagnostics = Vec::new(); + let mut candidates = Vec::new(); let mut missing_rank_count = 0usize; let mut missing_rank_examples = Vec::new(); let quote_usage = self.stock_filter_quote_usage(); @@ -10932,33 +10926,20 @@ impl PlatformExprStrategy { } else { SelectionRiskDeferral::None }; - let (universe_symbol_ids, _) = self.selection_symbol_ids_and_risk_decisions_with_options( + let universe = self.selectable_universe_on_with_options( ctx, date, universe_factor_date, selection_risk_deferral, - false, ); - let mut candidates = Vec::with_capacity(universe_symbol_ids.len()); - let mut candidate_states = Vec::with_capacity(universe_symbol_ids.len()); 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); let quote_candidate_limit = self.quote_plan_candidate_limit(selection_limit); - for symbol_id in universe_symbol_ids { - let factor = universe_factor_day - .factor(symbol_id) - .expect("market-cap order references missing factor row"); - let candidate = EligibleUniverseSnapshot { - symbol: factor.symbol.clone(), - market_cap_bn: decision_market_cap_bn(factor), - free_float_cap_bn: decision_free_float_cap_bn(factor), - }; - let stock = self.uncached_selection_stock_state_from_views_by_symbol_id( + for candidate in universe { + let stock = self.selection_stock_state_with_factor_date_from_views( ctx, date, stock_factor_date, - symbol_id, &candidate.symbol, &execution_day, &factor_day, @@ -10996,9 +10977,7 @@ impl PlatformExprStrategy { } continue; } - let state_index = candidate_states.len(); - candidate_states.push(stock); - candidates.push((candidate.symbol.clone(), rank_value, state_index)); + candidates.push((candidate.symbol.clone(), rank_value, stock)); } candidates.sort_by(|lhs, rhs| { let ordering = if self.config.rank_desc { @@ -11020,10 +10999,9 @@ impl PlatformExprStrategy { let mut quote_candidate_symbols = Vec::new(); let mut selected_symbols = Vec::new(); let mut processed_scope = 0usize; - for (symbol, _, state_index) in &candidates { - let stock = &candidate_states[*state_index]; + for (symbol, _, stock) in &candidates { processed_scope += 1; - if !self.stock_passes_quote_plan_filter(ctx, day, stock, quote_usage)? { + if !self.stock_passes_quote_plan_filter(ctx, day, &stock, quote_usage)? { if diagnostics.len() < 12 { diagnostics.push(format!("{symbol} quote_plan rejected by stock_expr")); } @@ -11041,7 +11019,7 @@ impl PlatformExprStrategy { continue; } if quote_usage == StockFilterQuoteUsage::IntradayQuote - && !self.stock_passes_expr(ctx, day, stock)? + && !self.stock_passes_expr(ctx, day, &stock)? { if diagnostics.len() < 12 { diagnostics.push(format!("{symbol} quote_plan rejected by stock_expr"));