From 75ab0c06c66761d7b2b0edb3359359e13d1265e3 Mon Sep 17 00:00:00 2001 From: boris Date: Sat, 5 Sep 2026 02:42:47 +0800 Subject: [PATCH] perf: type static current rolling helpers --- crates/fidc-core/src/data.rs | 73 +++++++++++------ .../fidc-core/src/platform_expr_strategy.rs | 79 ++++++++++++++++++- 2 files changed, 126 insertions(+), 26 deletions(-) diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index 0753b4b..177e41b 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -3454,31 +3454,13 @@ impl DataSet { let field = normalized_field(field); match field.as_ref() { "close" | "prev_close" | "stock_close" | "price" => self - .adjusted_close_series_by_symbol_id(symbol_id) - .and_then(|series| { - series_end - .map(|end| series.moving_average_at_end(end, lookback)) - .unwrap_or_else(|| series.current_moving_average(date, lookback)) - }), - "volume" | "stock_volume" => { - self.market_series_by_symbol_id(symbol_id) - .and_then(|series| { - series_end - .map(|end| { - series - .valid_volume_window(end, lookback) - .map(|(start, end)| { - normalize_rolling_factor( - (series.valid_volume_sum_prefix[end] - - series.valid_volume_sum_prefix[start]) - / lookback as f64, - 12, - ) - }) - }) - .unwrap_or_else(|| series.current_volume_moving_average(date, lookback)) - }) - } + .market_current_close_moving_average_with_end_by_symbol_id( + date, symbol_id, lookback, series_end, + ), + "volume" | "stock_volume" => self + .market_current_volume_moving_average_with_end_by_symbol_id( + date, symbol_id, lookback, series_end, + ), "day_open" | "dayopen" => { self.market_series_by_symbol_id(symbol_id) .and_then(|series| { @@ -3514,6 +3496,47 @@ impl DataSet { } } + pub(crate) fn market_current_close_moving_average_with_end_by_symbol_id( + &self, + date: NaiveDate, + symbol_id: u32, + lookback: usize, + series_end: Option, + ) -> Option { + self.adjusted_close_series_by_symbol_id(symbol_id) + .and_then(|series| { + series_end + .map(|end| series.moving_average_at_end(end, lookback)) + .unwrap_or_else(|| series.current_moving_average(date, lookback)) + }) + } + + pub(crate) fn market_current_volume_moving_average_with_end_by_symbol_id( + &self, + date: NaiveDate, + symbol_id: u32, + lookback: usize, + series_end: Option, + ) -> Option { + self.market_series_by_symbol_id(symbol_id) + .and_then(|series| { + series_end + .map(|end| { + series + .valid_volume_window(end, lookback) + .map(|(start, end)| { + normalize_rolling_factor( + (series.valid_volume_sum_prefix[end] + - series.valid_volume_sum_prefix[start]) + / lookback as f64, + 12, + ) + }) + }) + .unwrap_or_else(|| series.current_volume_moving_average(date, lookback)) + }) + } + pub fn market_latest_back_adjusted_close(&self, date: NaiveDate, symbol: &str) -> Option { self.adjusted_close_series(symbol) .and_then(|series| series.latest_back_adjusted_close(date)) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index be9cbfa..6a41155 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -972,6 +972,10 @@ enum CompiledRuntimeHelperArgs { lookback: usize, current: bool, }, + StockCurrentRollingMean { + field: StockRollingField, + lookback: usize, + }, RollingMaxCurrent { field: String, lookback: usize, @@ -5124,6 +5128,41 @@ impl PlatformExprStrategy { }; Ok(RuntimeHelperResolution::Number(value)) } + CompiledRuntimeHelperArgs::StockCurrentRollingMean { field, lookback } => { + let stock = stock.ok_or_else(|| { + BacktestError::Execution(format!( + "rolling_mean_current requires stock context for lookback {lookback}" + )) + })?; + let value = match field { + StockRollingField::Close => ctx + .data + .market_current_close_moving_average_with_end_by_symbol_id( + day.date, + stock.symbol_id, + *lookback, + stock.current_series_end, + ), + StockRollingField::Volume => ctx + .data + .market_current_volume_moving_average_with_end_by_symbol_id( + day.date, + stock.symbol_id, + *lookback, + stock.current_series_end, + ), + } + .ok_or_else(|| { + BacktestError::Execution(format!( + "missing current rolling mean for field {} with lookback {lookback}", + match field { + StockRollingField::Close => "close", + StockRollingField::Volume => "volume", + } + )) + })?; + Ok(RuntimeHelperResolution::Number(value)) + } CompiledRuntimeHelperArgs::RollingMaxCurrent { field, lookback } => { let values = self.resolve_current_rolling_values(ctx, day, stock, field, *lookback)?; @@ -6216,6 +6255,19 @@ impl PlatformExprStrategy { } "rolling_mean_current" => { let (field, lookback) = field_lookback()?; + let stock_field = match field.trim().to_ascii_lowercase().as_str() { + "close" | "prev_close" | "stock_close" | "price" => { + Some(StockRollingField::Close) + } + "volume" | "stock_volume" => Some(StockRollingField::Volume), + _ => None, + }; + if let Some(field) = stock_field { + return Some(CompiledRuntimeHelperArgs::StockCurrentRollingMean { + field, + lookback, + }); + } Some(CompiledRuntimeHelperArgs::RollingMean { field, lookback, @@ -12675,7 +12727,7 @@ mod tests { PlatformExprStrategyConfig, PlatformPortfolioDrawdownControlConfig, PlatformPortfolioDrawdownController, PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformStopTakeReferencePriceMode, PlatformTradeAction, PlatformUniverseActionKind, - RuntimeHelperResolution, SelectionRiskDeferral, StockFilterQuoteUsage, + RuntimeHelperResolution, SelectionRiskDeferral, StockFilterQuoteUsage, StockRollingField, StockSnapshotFieldRequirements, framework_stock_rolling_factor_requirement, scheduled_position_exposure, }; @@ -33593,6 +33645,31 @@ let target_exposure = csi_ready ? dynamic_exposure : 0.0; .all(|binding| binding.compiled_args.is_some()), "all static numeric helper arguments must be parsed once at plan compilation" ); + assert!(bindings.iter().any(|binding| { + if binding.name != "rolling_mean_current" { + return false; + } + matches!( + binding.compiled_args.as_ref(), + Some(CompiledRuntimeHelperArgs::StockCurrentRollingMean { + field: StockRollingField::Volume, + lookback: 10, + }) + ) + })); + + let signal_args = vec!["\"signal_close\"".to_string(), "20".to_string()]; + assert!(matches!( + PlatformExprStrategy::compile_runtime_helper_args( + "rolling_mean_current", + &signal_args + ), + Some(CompiledRuntimeHelperArgs::RollingMean { + field, + lookback: 20, + current: true, + }) if field == "signal_close" + )); } #[test]