diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index 5eceec1..bc72f70 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::collections::{BTreeMap, HashMap, HashSet}; use std::sync::{Arc, OnceLock}; @@ -959,7 +960,7 @@ impl SymbolPriceSeries { } fn numeric_value_at(&self, index: usize, field: &str) -> Option { - match normalize_field(field).as_str() { + match normalized_field(field).as_ref() { "day_open" | "dayopen" => Some(self.day_opens[index]), "open" => Some(self.opens[index]), "high" => Some(self.highs[index]), @@ -1405,12 +1406,14 @@ impl DataSet { .and_then(|rows| find_arc_by_symbol(rows, symbol, |row| row.symbol.as_str())) } - fn market_series(&self, symbol: &str) -> Option> { - self.market_series_by_symbol.get(symbol).cloned() + fn market_series(&self, symbol: &str) -> Option<&SymbolPriceSeries> { + self.market_series_by_symbol.get(symbol).map(Arc::as_ref) } - fn adjusted_close_series(&self, symbol: &str) -> Option> { - self.adjusted_close_series_by_symbol.get(symbol).cloned() + fn adjusted_close_series(&self, symbol: &str) -> Option<&AdjustedCloseSeries> { + self.adjusted_close_series_by_symbol + .get(symbol) + .map(Arc::as_ref) } pub fn factor(&self, date: NaiveDate, symbol: &str) -> Option<&DailyFactorSnapshot> { @@ -2452,19 +2455,14 @@ impl DataSet { field: &str, lookback: usize, ) -> Option { - let field = normalize_field(field); - match field.as_str() { + let field = normalized_field(field); + match field.as_ref() { "close" | "prev_close" | "stock_close" | "price" => self .adjusted_close_series(symbol) .and_then(|series| series.decision_moving_average(date, lookback)), - "volume" | "stock_volume" => { - if !self.source_daily_volume_window_available(date, symbol, lookback, false) { - None - } else { - self.market_series(symbol) - .and_then(|series| series.decision_volume_moving_average(date, lookback)) - } - } + "volume" | "stock_volume" => self + .market_series(symbol) + .and_then(|series| series.decision_volume_moving_average(date, lookback)), "day_open" | "dayopen" => { self.market_moving_average(date, symbol, lookback, PriceField::DayOpen) } @@ -2483,22 +2481,14 @@ impl DataSet { field: &str, lookback: usize, ) -> Option { - let field = normalize_field(field); - match field.as_str() { + let field = normalized_field(field); + match field.as_ref() { "close" | "prev_close" | "stock_close" | "price" => self .adjusted_close_series(symbol) .and_then(|series| series.current_moving_average(date, lookback)), - "volume" | "stock_volume" => { - if !self.source_daily_volume_window_available(date, symbol, lookback, true) { - None - } else { - self.market_series(symbol) - .and_then(|series| series.current_volume_moving_average(date, lookback)) - .or_else(|| { - self.factor_moving_average(date, symbol, "daily_volume", lookback) - }) - } - } + "volume" | "stock_volume" => self + .market_series(symbol) + .and_then(|series| series.current_volume_moving_average(date, lookback)), "day_open" | "dayopen" => { self.market_moving_average(date, symbol, lookback, PriceField::DayOpen) } @@ -2525,21 +2515,16 @@ impl DataSet { if lookback == 0 { return Vec::new(); } - let field = normalize_field(field); - match field.as_str() { + let field = normalized_field(field); + match field.as_ref() { "close" | "prev_close" | "stock_close" | "price" => self .adjusted_close_series(symbol) .map(|series| series.values(date, lookback, false)) .unwrap_or_default(), - "volume" | "stock_volume" => { - if !self.source_daily_volume_window_available(date, symbol, lookback, false) { - Vec::new() - } else { - self.market_series(symbol) - .and_then(|series| series.decision_volume_values(date, lookback)) - .unwrap_or_default() - } - } + "volume" | "stock_volume" => self + .market_series(symbol) + .and_then(|series| series.decision_volume_values(date, lookback)) + .unwrap_or_default(), "day_open" | "dayopen" => self .market_series(symbol) .map(|series| series.trailing_values(date, lookback, PriceField::DayOpen)) @@ -2563,14 +2548,9 @@ impl DataSet { field: &str, lookback: usize, ) -> Vec { - let field = normalize_field(field); - if matches!(field.as_str(), "volume" | "stock_volume") - && !self.source_daily_volume_window_available(date, symbol, lookback, true) - { - return Vec::new(); - } + let field = normalized_field(field); if matches!( - field.as_str(), + field.as_ref(), "close" | "prev_close" | "stock_close" | "price" ) { return self @@ -2578,40 +2558,17 @@ impl DataSet { .map(|series| series.values(date, lookback, true)) .unwrap_or_default(); } - if matches!(field.as_str(), "volume" | "stock_volume") { + if matches!(field.as_ref(), "volume" | "stock_volume") { return self .market_series(symbol) .and_then(|series| series.current_volume_values(date, lookback)) .unwrap_or_default(); } self.market_series(symbol) - .map(|series| series.trailing_numeric_values(date, lookback, &field, true)) + .map(|series| series.trailing_numeric_values(date, lookback, field.as_ref(), true)) .unwrap_or_default() } - fn source_daily_volume_window_available( - &self, - date: NaiveDate, - symbol: &str, - lookback: usize, - include_now: bool, - ) -> bool { - if lookback == 0 { - return false; - } - let Some(series) = self.market_series(symbol) else { - return false; - }; - let end_index = if include_now { - series.end_index(date) - } else { - series.previous_completed_end_index(date) - }; - end_index - .and_then(|end| series.valid_volume_window(end, lookback)) - .is_some() - } - pub fn factor_numeric_values( &self, date: NaiveDate, @@ -3034,11 +2991,16 @@ fn intraday_quote_price_bar(snapshot: &IntradayExecutionQuote) -> PriceBar { } fn normalize_field(field: &str) -> String { - field - .trim() - .trim_matches('"') - .trim_matches('\'') - .to_ascii_lowercase() + normalized_field(field).into_owned() +} + +fn normalized_field(field: &str) -> Cow<'_, str> { + let trimmed = field.trim().trim_matches('"').trim_matches('\''); + if trimmed.bytes().all(|byte| !byte.is_ascii_uppercase()) { + Cow::Borrowed(trimmed) + } else { + Cow::Owned(trimmed.to_ascii_lowercase()) + } } fn normalize_factor_snapshots(factors: Vec) -> Vec { diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index f3a9fbe..16342d6 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -8432,7 +8432,6 @@ impl PlatformExprStrategy { && Self::expr_requires_stock_extra_factors( &config.stock_filter_expr, prelude_declared_identifiers, - true, ) { return true; @@ -8444,9 +8443,7 @@ impl PlatformExprStrategy { config.rank_expr.as_str(), ] .into_iter() - .any(|expr| { - Self::expr_requires_stock_extra_factors(expr, prelude_declared_identifiers, true) - }) + .any(|expr| Self::expr_requires_stock_extra_factors(expr, prelude_declared_identifiers)) } fn stock_extra_factor_identifiers_for_config( @@ -8468,7 +8465,6 @@ impl PlatformExprStrategy { &mut identifiers, &config.stock_filter_expr, prelude_declared_identifiers, - true, ); } for expr in [ @@ -8481,7 +8477,6 @@ impl PlatformExprStrategy { &mut identifiers, expr, prelude_declared_identifiers, - true, ); } identifiers @@ -8491,7 +8486,6 @@ impl PlatformExprStrategy { out: &mut BTreeSet, expr: &str, prelude_declared_identifiers: &BTreeSet, - stock_rolling_helpers_require_extra: bool, ) { let normalized = Self::normalize_expr(expr); for name in Self::extract_identifier_candidates(&normalized) { @@ -8503,11 +8497,6 @@ impl PlatformExprStrategy { { continue; } - if stock_rolling_helpers_require_extra - && matches!(name.as_str(), "rolling_mean" | "sma" | "ma" | "vma") - { - continue; - } out.insert(name); } } @@ -8558,23 +8547,12 @@ impl PlatformExprStrategy { fn expr_requires_stock_extra_factors( expr: &str, prelude_declared_identifiers: &BTreeSet, - stock_rolling_helpers_require_extra: bool, ) -> bool { let normalized = Self::normalize_expr(expr); let identifiers = Self::extract_identifier_candidates(&normalized); if identifiers.contains("factors") || identifiers.contains("factor") { return true; } - if stock_rolling_helpers_require_extra - && identifiers.iter().any(|name| { - matches!( - name.as_str(), - "rolling_mean" | "rolling_mean_current" | "sma" | "ma" | "vma" - ) - }) - { - return true; - } identifiers.into_iter().any(|name| { !Self::is_expression_keyword(&name) && !Self::is_runtime_helper(&name) @@ -8717,11 +8695,6 @@ impl PlatformExprStrategy { ) { 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, - "rolling_mean_current", - ); 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_vma_helper(requirements, &compact); @@ -12614,6 +12587,24 @@ mod tests { ); } + #[test] + fn current_rolling_helpers_do_not_load_factor_maps_or_decision_rollings() { + let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.stock_filter_expr = concat!( + "rolling_mean_current(\"close\", 5) > rolling_mean_current(\"close\", 10)", + " && rolling_mean_current(\"volume\", 5) < rolling_mean_current(\"volume\", 100)" + ) + .to_string(); + + let strategy = PlatformExprStrategy::new(cfg); + + assert!(!strategy.stock_extra_factors_required); + assert!(!strategy.stock_rolling_requirements.requires("close", 5)); + assert!(!strategy.stock_rolling_requirements.requires("close", 10)); + assert!(!strategy.stock_rolling_requirements.requires("volume", 5)); + assert!(!strategy.stock_rolling_requirements.requires("volume", 100)); + } + #[test] fn platform_expr_missing_requested_factor_does_not_default_to_zero() { let date = d(2025, 5, 19);