diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 7eb9739..e95b083 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -706,10 +706,59 @@ struct StockExpressionState { stock_volume_ma20: f64, stock_volume_ma60: f64, stock_volume_ma100: f64, + stock_current_ma5: f64, + stock_current_ma10: f64, + stock_current_ma20: f64, + stock_current_ma30: f64, + stock_current_volume_ma5: f64, + stock_current_volume_ma10: f64, + stock_current_volume_ma20: f64, + stock_current_volume_ma60: f64, + stock_current_volume_ma100: f64, extra_factors: BTreeMap, extra_text_factors: BTreeMap, } +impl StockExpressionState { + fn standard_rolling_mean( + &self, + field: &str, + lookback: usize, + include_now: bool, + ) -> Option { + let value = match (field, lookback, include_now) { + ("close" | "prev_close" | "stock_close" | "price", 5, false) => self.stock_ma5, + ("close" | "prev_close" | "stock_close" | "price", 10, false) => self.stock_ma10, + ("close" | "prev_close" | "stock_close" | "price", 20, false) => self.stock_ma20, + ("close" | "prev_close" | "stock_close" | "price", 30, false) => self.stock_ma30, + ("volume" | "stock_volume", 5, false) => self.stock_volume_ma5, + ("volume" | "stock_volume", 10, false) => self.stock_volume_ma10, + ("volume" | "stock_volume", 20, false) => self.stock_volume_ma20, + ("volume" | "stock_volume", 60, false) => self.stock_volume_ma60, + ("volume" | "stock_volume", 100, false) => self.stock_volume_ma100, + ("close" | "prev_close" | "stock_close" | "price", 5, true) => { + self.stock_current_ma5 + } + ("close" | "prev_close" | "stock_close" | "price", 10, true) => { + self.stock_current_ma10 + } + ("close" | "prev_close" | "stock_close" | "price", 20, true) => { + self.stock_current_ma20 + } + ("close" | "prev_close" | "stock_close" | "price", 30, true) => { + self.stock_current_ma30 + } + ("volume" | "stock_volume", 5, true) => self.stock_current_volume_ma5, + ("volume" | "stock_volume", 10, true) => self.stock_current_volume_ma10, + ("volume" | "stock_volume", 20, true) => self.stock_current_volume_ma20, + ("volume" | "stock_volume", 60, true) => self.stock_current_volume_ma60, + ("volume" | "stock_volume", 100, true) => self.stock_current_volume_ma100, + _ => return None, + }; + value.is_finite().then_some(value) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum StockFilterQuoteUsage { DailyOnly, @@ -727,6 +776,7 @@ enum StockRollingField { struct StockRollingRequirements { all: bool, fields: BTreeSet<(StockRollingField, usize)>, + current_fields: BTreeSet<(StockRollingField, usize)>, } impl StockRollingRequirements { @@ -736,6 +786,12 @@ impl StockRollingRequirements { } } + fn require_current(&mut self, field: StockRollingField, lookback: usize) { + if lookback > 0 { + self.current_fields.insert((field, lookback)); + } + } + fn require_all(&mut self) { self.all = true; } @@ -751,6 +807,19 @@ impl StockRollingRequirements { }; self.fields.contains(&(field, lookback)) } + + fn requires_current(&self, field: &'static str, lookback: usize) -> bool { + let field = match field { + "close" => StockRollingField::Close, + "volume" => StockRollingField::Volume, + _ => return false, + }; + self.current_fields.contains(&(field, lookback)) + } + + fn has_current(&self) -> bool { + !self.current_fields.is_empty() + } } #[derive(Debug, Clone)] @@ -3943,6 +4012,12 @@ impl PlatformExprStrategy { .then_some(lookback) .unwrap_or(0) }; + let required_current_rolling = |field: &'static str, lookback: usize| { + self.stock_rolling_requirements + .requires_current(field, lookback) + .then_some(lookback) + .unwrap_or(0) + }; let close_lookbacks = [ required_rolling("close", self.config.stock_short_ma_days), required_rolling("close", self.config.stock_mid_ma_days), @@ -3959,6 +4034,22 @@ impl PlatformExprStrategy { required_rolling("volume", 60), required_rolling("volume", 100), ]; + let current_close_lookbacks = [ + 0, + 0, + 0, + required_current_rolling("close", 5), + required_current_rolling("close", 10), + required_current_rolling("close", 20), + required_current_rolling("close", 30), + ]; + let current_volume_lookbacks = [ + required_current_rolling("volume", 5), + required_current_rolling("volume", 10), + required_current_rolling("volume", 20), + required_current_rolling("volume", 60), + required_current_rolling("volume", 100), + ]; let rolling_means = ctx.data.market_standard_rolling_means_by_symbol_id( date, symbol_id, @@ -3966,8 +4057,26 @@ impl PlatformExprStrategy { &volume_lookbacks, false, ); + let current_rolling_means = if self.stock_rolling_requirements.has_current() { + ctx.data.market_standard_rolling_means_by_symbol_id( + date, + symbol_id, + ¤t_close_lookbacks, + ¤t_volume_lookbacks, + true, + ) + } else { + crate::data::StandardRollingMeans { + close: [None; 7], + volume: [None; 5], + } + }; let close_rolling = |index: usize| rolling_means.close[index].unwrap_or(f64::NAN); let volume_rolling = |index: usize| rolling_means.volume[index].unwrap_or(f64::NAN); + let current_close_rolling = + |index: usize| current_rolling_means.close[index].unwrap_or(f64::NAN); + let current_volume_rolling = + |index: usize| current_rolling_means.volume[index].unwrap_or(f64::NAN); let stock_ma_short = close_rolling(0); let stock_ma_mid = close_rolling(1); let stock_ma_long = close_rolling(2); @@ -3980,6 +4089,15 @@ impl PlatformExprStrategy { let stock_volume_ma20 = volume_rolling(2); let stock_volume_ma60 = volume_rolling(3); let stock_volume_ma100 = volume_rolling(4); + let stock_current_ma5 = current_close_rolling(3); + let stock_current_ma10 = current_close_rolling(4); + let stock_current_ma20 = current_close_rolling(5); + let stock_current_ma30 = current_close_rolling(6); + let stock_current_volume_ma5 = current_volume_rolling(0); + let stock_current_volume_ma10 = current_volume_rolling(1); + let stock_current_volume_ma20 = current_volume_rolling(2); + let stock_current_volume_ma60 = current_volume_rolling(3); + let stock_current_volume_ma100 = current_volume_rolling(4); let touched_upper_limit = if intraday_same_day_factor { !market.paused && (market.is_at_upper_limit_price(market.close) @@ -4126,6 +4244,15 @@ impl PlatformExprStrategy { stock_volume_ma20, stock_volume_ma60, stock_volume_ma100, + stock_current_ma5, + stock_current_ma10, + stock_current_ma20, + stock_current_ma30, + stock_current_volume_ma5, + stock_current_volume_ma10, + stock_current_volume_ma20, + stock_current_volume_ma60, + stock_current_volume_ma100, extra_factors, extra_text_factors: if self.stock_text_factors_required { ctx.data @@ -6700,14 +6827,16 @@ impl PlatformExprStrategy { "rolling_mean(\"{other}\", {lookback}) requires stock context" )) })?; - self.stock_decision_rolling_mean( - ctx, - day.date, - stock.symbol_id, - &stock.symbol, - other, - lookback, - ) + stock.standard_rolling_mean(other, lookback, false).or_else(|| { + self.stock_decision_rolling_mean( + ctx, + day.date, + stock.symbol_id, + &stock.symbol, + other, + lookback, + ) + }) } }; value.ok_or_else(|| { @@ -6749,14 +6878,16 @@ impl PlatformExprStrategy { "rolling_mean_current(\"{other}\", {lookback}) requires stock context" )) })?; - self.stock_current_rolling_mean( - ctx, - day.date, - stock.symbol_id, - &stock.symbol, - other, - lookback, - ) + stock.standard_rolling_mean(other, lookback, true).or_else(|| { + self.stock_current_rolling_mean( + ctx, + day.date, + stock.symbol_id, + &stock.symbol, + other, + lookback, + ) + }) } }; value.ok_or_else(|| { @@ -9852,9 +9983,32 @@ impl PlatformExprStrategy { expr: &str, ) { let compact = Self::compact_expr(expr); - Self::require_stock_rollings_for_named_helper(requirements, &compact, "rolling_mean"); - Self::require_stock_rollings_for_named_helper(requirements, &compact, "sma"); - Self::require_stock_rollings_for_named_helper(requirements, &compact, "ma"); + Self::require_stock_rollings_for_named_helper( + requirements, + &compact, + "rolling_mean", + false, + ); + Self::require_stock_rollings_for_named_helper(requirements, &compact, "sma", false); + Self::require_stock_rollings_for_named_helper(requirements, &compact, "ma", false); + Self::require_stock_rollings_for_named_helper( + requirements, + &compact, + "rolling_mean_current", + true, + ); + Self::require_stock_rollings_for_named_helper( + requirements, + &compact, + "rolling_max_current", + true, + ); + Self::require_stock_rollings_for_named_helper( + requirements, + &compact, + "rolling_return_stddev_current", + true, + ); Self::require_stock_rollings_for_vma_helper(requirements, &compact); } @@ -9862,6 +10016,7 @@ impl PlatformExprStrategy { requirements: &mut StockRollingRequirements, compact: &str, helper: &str, + current: bool, ) { let needle = format!("{helper}("); let mut offset = 0usize; @@ -9883,10 +10038,18 @@ impl PlatformExprStrategy { let normalized_field = field.trim().to_ascii_lowercase(); match normalized_field.as_str() { "close" | "prev_close" | "stock_close" | "price" => { - requirements.require(StockRollingField::Close, lookback) + if current { + requirements.require_current(StockRollingField::Close, lookback) + } else { + requirements.require(StockRollingField::Close, lookback) + } } "volume" | "stock_volume" => { - requirements.require(StockRollingField::Volume, lookback) + if current { + requirements.require_current(StockRollingField::Volume, lookback) + } else { + requirements.require(StockRollingField::Volume, lookback) + } } _ => {} } @@ -13750,7 +13913,7 @@ mod tests { } #[test] - fn current_rolling_helpers_do_not_load_factor_maps_or_decision_rollings() { + fn current_rolling_helpers_load_only_current_standard_rollings() { let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.stock_filter_expr = concat!( "rolling_mean_current(\"close\", 5) > rolling_mean_current(\"close\", 10)", @@ -13765,6 +13928,18 @@ mod tests { assert!(!strategy.stock_rolling_requirements.requires("close", 10)); assert!(!strategy.stock_rolling_requirements.requires("volume", 5)); assert!(!strategy.stock_rolling_requirements.requires("volume", 100)); + assert!(strategy + .stock_rolling_requirements + .requires_current("close", 5)); + assert!(strategy + .stock_rolling_requirements + .requires_current("close", 10)); + assert!(strategy + .stock_rolling_requirements + .requires_current("volume", 5)); + assert!(strategy + .stock_rolling_requirements + .requires_current("volume", 100)); } #[test]