diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index aea971d..936a5cc 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -553,14 +553,12 @@ pub struct PlatformExprStrategy { prelude_numeric_constants: HashMap, compact_stock_filter_expr: String, stock_filter_quote_usage: StockFilterQuoteUsage, + selection_quote_usage: StockFilterQuoteUsage, stock_rolling_requirements: StockRollingRequirements, stock_extra_factors_required: bool, stock_state_cache_date: RefCell>, - stock_state_cache: - RefCell), StockExpressionState>>, - scheduled_quote_cache_date: RefCell>, - scheduled_quote_cache: RefCell< - HashMap<(NaiveDate, String, NaiveDateTime), Option>, + stock_state_cache: RefCell< + HashMap<(NaiveDate, NaiveDate, String, Option, bool), StockExpressionState>, >, } @@ -649,6 +647,8 @@ impl PlatformExprStrategy { let compact_stock_filter_expr = Self::compact_expr(&normalized_stock_filter_expr); let stock_filter_quote_usage = Self::stock_filter_quote_usage_for_expr(&normalized_stock_filter_expr); + let selection_quote_usage = + Self::selection_quote_usage_for_config(&config, &normalized_stock_filter_expr); let stock_rolling_requirements = Self::stock_rolling_requirements_for_config(&config, &normalized_stock_filter_expr); let stock_extra_factors_required = Self::stock_extra_factors_required_for_config( @@ -672,12 +672,11 @@ impl PlatformExprStrategy { prelude_numeric_constants, compact_stock_filter_expr, stock_filter_quote_usage, + selection_quote_usage, stock_rolling_requirements, stock_extra_factors_required, stock_state_cache_date: RefCell::new(None), stock_state_cache: RefCell::new(HashMap::new()), - scheduled_quote_cache_date: RefCell::new(None), - scheduled_quote_cache: RefCell::new(HashMap::new()), } } @@ -1198,22 +1197,22 @@ impl PlatformExprStrategy { ) } - fn aiquant_scheduled_quote( + fn aiquant_scheduled_quote<'a>( &self, - ctx: &StrategyContext<'_>, + ctx: &'a StrategyContext<'_>, date: NaiveDate, symbol: &str, - ) -> Option { + ) -> Option<&'a crate::data::IntradayExecutionQuote> { self.aiquant_scheduled_quote_at_time(ctx, date, symbol, None) } - fn aiquant_scheduled_quote_at_time( + fn aiquant_scheduled_quote_at_time<'a>( &self, - ctx: &StrategyContext<'_>, + ctx: &'a StrategyContext<'_>, date: NaiveDate, symbol: &str, execution_time: Option, - ) -> Option { + ) -> Option<&'a crate::data::IntradayExecutionQuote> { if !self.config.aiquant_transaction_cost { return None; } @@ -1224,28 +1223,11 @@ impl PlatformExprStrategy { &ProjectedExecutionState::default(), execution_time, ); - { - let mut cache_date = self.scheduled_quote_cache_date.borrow_mut(); - if *cache_date != Some(date) { - self.scheduled_quote_cache.borrow_mut().clear(); - *cache_date = Some(date); - } - } - let cache_key = (date, symbol.to_string(), start_cursor); - if let Some(cached) = self.scheduled_quote_cache.borrow().get(&cache_key) { - return cached.clone(); - } - let quote = ctx - .data + ctx.data .execution_quotes_on(date, symbol) .iter() .filter(|quote| quote.timestamp <= start_cursor) .max_by_key(|quote| quote.timestamp) - .cloned(); - self.scheduled_quote_cache - .borrow_mut() - .insert(cache_key, quote.clone()); - quote } fn aiquant_scheduled_buy_price( @@ -1255,7 +1237,7 @@ impl PlatformExprStrategy { symbol: &str, ) -> Option { self.aiquant_scheduled_quote(ctx, date, symbol) - .and_then(|quote| self.projected_quote_raw_price("e, OrderSide::Buy)) + .and_then(|quote| self.projected_quote_raw_price(quote, OrderSide::Buy)) } fn aiquant_scheduled_sell_price_at_time( @@ -1266,7 +1248,7 @@ impl PlatformExprStrategy { execution_time: Option, ) -> Option { self.aiquant_scheduled_quote_at_time(ctx, date, symbol, execution_time) - .and_then(|quote| self.projected_quote_raw_price("e, OrderSide::Sell)) + .and_then(|quote| self.projected_quote_raw_price(quote, OrderSide::Sell)) } fn aiquant_scheduled_last_price( @@ -2211,7 +2193,25 @@ impl PlatformExprStrategy { factor_date: NaiveDate, symbol: &str, ) -> Result { - self.stock_state_with_factor_date_and_time(ctx, date, factor_date, symbol, None) + self.stock_state_with_factor_date_and_time(ctx, date, factor_date, symbol, None, true) + } + + fn selection_stock_state_with_factor_date( + &self, + ctx: &StrategyContext<'_>, + date: NaiveDate, + factor_date: NaiveDate, + symbol: &str, + ) -> Result { + let use_intraday_quote = self.selection_quote_usage != StockFilterQuoteUsage::DailyOnly; + self.stock_state_with_factor_date_and_time( + ctx, + date, + factor_date, + symbol, + None, + use_intraday_quote, + ) } fn stock_decision_rolling_mean( @@ -2244,7 +2244,7 @@ impl PlatformExprStrategy { symbol: &str, execution_time: Option, ) -> Result { - self.stock_state_with_factor_date_and_time(ctx, date, date, symbol, execution_time) + self.stock_state_with_factor_date_and_time(ctx, date, date, symbol, execution_time, true) } fn stock_is_at_upper_limit_at_time( @@ -2313,6 +2313,7 @@ impl PlatformExprStrategy { factor_date: NaiveDate, symbol: &str, execution_time: Option, + use_intraday_quote: bool, ) -> Result { { let mut cache_date = self.stock_state_cache_date.borrow_mut(); @@ -2321,7 +2322,13 @@ impl PlatformExprStrategy { *cache_date = Some(date); } } - let cache_key = (date, factor_date, symbol.to_string(), execution_time); + let cache_key = ( + date, + factor_date, + symbol.to_string(), + execution_time, + use_intraday_quote, + ); if let Some(state) = self.stock_state_cache.borrow().get(&cache_key) { return Ok(state.clone()); } @@ -2329,8 +2336,11 @@ impl PlatformExprStrategy { let market = ctx.data.require_market(date, symbol)?; let feature_market = ctx.data.market(factor_date, symbol).unwrap_or(market); let intraday_same_day_factor = self.config.aiquant_transaction_cost && factor_date == date; - let decision_quote = - self.aiquant_scheduled_quote_at_time(ctx, date, symbol, execution_time); + let decision_quote = if use_intraday_quote { + self.aiquant_scheduled_quote_at_time(ctx, date, symbol, execution_time) + } else { + None + }; let factor = ctx.data.require_factor(factor_date, symbol)?; let candidate = ctx.data.require_candidate(date, symbol)?; let instrument = ctx.data.instrument(symbol); @@ -2451,12 +2461,11 @@ impl PlatformExprStrategy { low: expression_low, close: expression_close, last: decision_quote - .as_ref() .and_then(|quote| { (quote.last_price.is_finite() && quote.last_price > 0.0) .then_some(quote.last_price) }) - .or_else(|| decision_quote.as_ref().and_then(|quote| quote.buy_price())) + .or_else(|| decision_quote.and_then(|quote| quote.buy_price())) .unwrap_or(market.last_price), prev_close: feature_market.prev_close, amount, @@ -6208,8 +6217,12 @@ impl PlatformExprStrategy { let mut diagnostics = Vec::new(); let mut candidates = Vec::new(); for candidate in universe { - let stock = - self.stock_state_with_factor_date(ctx, date, stock_factor_date, &candidate.symbol)?; + let stock = self.selection_stock_state_with_factor_date( + ctx, + date, + stock_factor_date, + &candidate.symbol, + )?; let field_value = self.selection_field_value(&candidate, &stock); if !field_value.is_finite() { if diagnostics.len() < 12 { @@ -6296,6 +6309,34 @@ impl PlatformExprStrategy { usage } + fn merge_quote_usage( + lhs: StockFilterQuoteUsage, + rhs: StockFilterQuoteUsage, + ) -> StockFilterQuoteUsage { + match (lhs, rhs) { + (StockFilterQuoteUsage::IntradayQuote, _) + | (_, StockFilterQuoteUsage::IntradayQuote) => StockFilterQuoteUsage::IntradayQuote, + (StockFilterQuoteUsage::LimitStateOnly, _) + | (_, StockFilterQuoteUsage::LimitStateOnly) => StockFilterQuoteUsage::LimitStateOnly, + _ => StockFilterQuoteUsage::DailyOnly, + } + } + + fn selection_quote_usage_for_config( + config: &PlatformExprStrategyConfig, + normalized_stock_filter_expr: &str, + ) -> StockFilterQuoteUsage { + [ + normalized_stock_filter_expr, + config.market_cap_field.as_str(), + config.rank_by.as_str(), + config.rank_expr.as_str(), + ] + .into_iter() + .map(Self::stock_filter_quote_usage_for_expr) + .fold(StockFilterQuoteUsage::DailyOnly, Self::merge_quote_usage) + } + fn stock_rolling_requirements_for_config( config: &PlatformExprStrategyConfig, normalized_stock_filter_expr: &str, @@ -6571,10 +6612,15 @@ impl PlatformExprStrategy { }) } + #[cfg(test)] fn stock_filter_uses_intraday_quote_fields(&self) -> bool { self.stock_filter_quote_usage() != StockFilterQuoteUsage::DailyOnly } + fn selection_uses_intraday_quote_fields(&self) -> bool { + self.selection_quote_usage != StockFilterQuoteUsage::DailyOnly + } + fn quote_plan_candidate_limit(&self, selection_limit: usize) -> usize { selection_limit .saturating_mul(3) @@ -6637,8 +6683,12 @@ impl PlatformExprStrategy { let quote_usage = self.stock_filter_quote_usage(); let quote_candidate_limit = self.quote_plan_candidate_limit(selection_limit); for candidate in universe { - let stock = - self.stock_state_with_factor_date(ctx, date, stock_factor_date, &candidate.symbol)?; + let stock = self.selection_stock_state_with_factor_date( + ctx, + date, + stock_factor_date, + &candidate.symbol, + )?; let field_value = self.selection_field_value(&candidate, &stock); if !field_value.is_finite() { if diagnostics.len() < 12 { @@ -6748,7 +6798,7 @@ impl PlatformExprStrategy { let day = self.day_state(ctx, ctx.decision_date)?; let (selection_date, universe_factor_date, stock_factor_date) = self.selection_dates(ctx); - let requires_intraday_selection_quotes = self.stock_filter_uses_intraday_quote_fields(); + let requires_intraday_selection_quotes = self.selection_uses_intraday_quote_fields(); let (band_low, band_high) = self.market_cap_band(ctx, &day)?; let selection_limit = self .selection_limit(ctx, &day)? @@ -20898,6 +20948,29 @@ mod tests { strategy.stock_filter_uses_intraday_quote_fields(), "actual tick/quote fields still require intraday selection quotes" ); + + let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.stock_filter_expr = "close > 1.0".to_string(); + cfg.rank_by = "last_price".to_string(); + let strategy = PlatformExprStrategy::new(cfg); + assert_eq!( + strategy.stock_filter_quote_usage(), + StockFilterQuoteUsage::DailyOnly + ); + assert!( + strategy.selection_uses_intraday_quote_fields(), + "selection ranking by tick fields must still request intraday selection quotes" + ); + + let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.stock_filter_expr = + "rolling_mean(\"close\", 5) > rolling_mean(\"close\", 10)".to_string(); + cfg.rank_by = "market_cap".to_string(); + let strategy = PlatformExprStrategy::new(cfg); + assert!( + !strategy.selection_uses_intraday_quote_fields(), + "pure daily rolling selection must not prefetch intraday quotes" + ); } #[test]