diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 21a8790..203d737 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -442,6 +442,42 @@ enum StockFilterQuoteUsage { IntradayQuote, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum StockRollingField { + Close, + Volume, +} + +#[derive(Debug, Clone, Default)] +struct StockRollingRequirements { + all: bool, + fields: BTreeSet<(StockRollingField, usize)>, +} + +impl StockRollingRequirements { + fn require(&mut self, field: StockRollingField, lookback: usize) { + if lookback > 0 { + self.fields.insert((field, lookback)); + } + } + + fn require_all(&mut self) { + self.all = true; + } + + fn requires(&self, field: &'static str, lookback: usize) -> bool { + if self.all { + return true; + } + let field = match field { + "close" => StockRollingField::Close, + "volume" => StockRollingField::Volume, + _ => return false, + }; + self.fields.contains(&(field, lookback)) + } +} + #[derive(Debug, Clone)] struct PositionExpressionState { order_book_id: String, @@ -517,6 +553,7 @@ pub struct PlatformExprStrategy { prelude_numeric_constants: HashMap, compact_stock_filter_expr: String, stock_filter_quote_usage: StockFilterQuoteUsage, + stock_rolling_requirements: StockRollingRequirements, stock_state_cache_date: RefCell>, stock_state_cache: RefCell), StockExpressionState>>, @@ -607,6 +644,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 stock_rolling_requirements = + Self::stock_rolling_requirements_for_config(&config, &normalized_stock_filter_expr); Self { config, engine, @@ -623,6 +662,7 @@ impl PlatformExprStrategy { prelude_numeric_constants, compact_stock_filter_expr, stock_filter_quote_usage, + stock_rolling_requirements, stock_state_cache_date: RefCell::new(None), stock_state_cache: RefCell::new(HashMap::new()), } @@ -2266,6 +2306,9 @@ impl PlatformExprStrategy { let instrument = ctx.data.instrument(symbol); let mut rolling_values = HashMap::<(&'static str, usize), f64>::new(); let mut rolling = |field: &'static str, lookback: usize| -> f64 { + if !self.stock_rolling_requirements.requires(field, lookback) { + return f64::NAN; + } *rolling_values.entry((field, lookback)).or_insert_with(|| { self.stock_decision_rolling_mean( ctx, @@ -6189,6 +6232,113 @@ impl PlatformExprStrategy { usage } + fn stock_rolling_requirements_for_config( + config: &PlatformExprStrategyConfig, + normalized_stock_filter_expr: &str, + ) -> StockRollingRequirements { + let mut requirements = StockRollingRequirements::default(); + let expressions = [ + config.prelude.as_str(), + config.stock_filter_expr.as_str(), + config.buy_scale_expr.as_str(), + config.stop_loss_expr.as_str(), + config.take_profit_expr.as_str(), + config.rank_expr.as_str(), + config.market_cap_field.as_str(), + ]; + for expr in expressions { + let normalized = Self::normalize_expr(expr); + if Self::extract_identifier_candidates(&normalized).contains("factors") { + requirements.require_all(); + return requirements; + } + Self::require_stock_rollings_for_identifiers(&mut requirements, config, &normalized); + } + Self::require_stock_rollings_for_fast_filter( + &mut requirements, + config, + normalized_stock_filter_expr, + ); + requirements + } + + fn require_stock_rollings_for_identifiers( + requirements: &mut StockRollingRequirements, + config: &PlatformExprStrategyConfig, + expr: &str, + ) { + for name in Self::extract_identifier_candidates(expr) { + match name.as_str() { + "stock_ma_short" => { + requirements.require(StockRollingField::Close, config.stock_short_ma_days) + } + "stock_ma_mid" => { + requirements.require(StockRollingField::Close, config.stock_mid_ma_days) + } + "stock_ma_long" => { + requirements.require(StockRollingField::Close, config.stock_long_ma_days) + } + "stock_ma5" | "ma5" => requirements.require(StockRollingField::Close, 5), + "stock_ma10" | "ma10" => requirements.require(StockRollingField::Close, 10), + "stock_ma20" | "ma20" => requirements.require(StockRollingField::Close, 20), + "stock_ma30" | "ma30" => requirements.require(StockRollingField::Close, 30), + "stock_volume_ma5" | "volume_ma5" | "vma5" => { + requirements.require(StockRollingField::Volume, 5) + } + "stock_volume_ma10" | "volume_ma10" | "vma10" => { + requirements.require(StockRollingField::Volume, 10) + } + "stock_volume_ma20" | "volume_ma20" | "vma20" => { + requirements.require(StockRollingField::Volume, 20) + } + "stock_volume_ma60" | "volume_ma60" | "vma60" => { + requirements.require(StockRollingField::Volume, 60) + } + "stock_volume_ma100" | "volume_ma100" | "vma100" => { + requirements.require(StockRollingField::Volume, 100) + } + _ => {} + } + } + } + + fn require_stock_rollings_for_fast_filter( + requirements: &mut StockRollingRequirements, + config: &PlatformExprStrategyConfig, + normalized_stock_filter_expr: &str, + ) { + let compact = Self::compact_expr(normalized_stock_filter_expr); + if compact == "stock_ma_short>stock_ma_mid*ma_ratio&&stock_ma_mid>stock_ma_long" { + requirements.require(StockRollingField::Close, config.stock_short_ma_days); + requirements.require(StockRollingField::Close, config.stock_mid_ma_days); + requirements.require(StockRollingField::Close, config.stock_long_ma_days); + return; + } + + let mut filter_body = compact.as_str(); + if let Some(rest) = filter_body.strip_prefix("listed_days>=min_listed_days&&") { + filter_body = rest; + } + let base_microcap_filter = "rolling_mean(\"close\",5)>rolling_mean(\"close\",10)*ma_ratio&&rolling_mean(\"close\",10)>rolling_mean(\"close\",30)*ma_ratio&&rolling_mean(\"volume\",5)0&&rolling_mean(\"volume\",100)>0" + | "&&rolling_mean(\"volume\",5)>0.0&&rolling_mean(\"volume\",100)>0.0" + ) + }); + if matches_microcap_fast_filter { + requirements.require(StockRollingField::Close, 5); + requirements.require(StockRollingField::Close, 10); + requirements.require(StockRollingField::Close, 30); + requirements.require(StockRollingField::Volume, 5); + requirements.require(StockRollingField::Volume, 100); + } + } + fn stock_filter_uses_intraday_quote_fields(&self) -> bool { self.stock_filter_quote_usage() != StockFilterQuoteUsage::DailyOnly } @@ -18735,6 +18885,7 @@ mod tests { }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.prefer_precomputed_rolling_factors = true; + cfg.stock_filter_expr = "stock_ma5 > 0 && stock_volume_ma5 > 0".to_string(); let strategy = PlatformExprStrategy::new(cfg); let stock = strategy .stock_state_with_factor_date(&ctx, date, date, symbol) @@ -18742,7 +18893,9 @@ mod tests { assert_eq!(stock.stock_ma5, 99.0); assert_eq!(stock.stock_volume_ma5, 88.0); - let strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); + let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.stock_filter_expr = "stock_ma5 > 0 && stock_volume_ma5 > 0".to_string(); + let strategy = PlatformExprStrategy::new(cfg); let stock = strategy .stock_state_with_factor_date(&ctx, date, date, symbol) .expect("stock state");