From cea079a7709607a8b171c1cdd8ee813724825088 Mon Sep 17 00:00:00 2001 From: boris Date: Mon, 24 Aug 2026 02:51:58 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=A4=8D=E6=9D=83=E6=BB=9A?= =?UTF-8?q?=E5=8A=A8=E5=9B=A0=E5=AD=90=E8=AE=A1=E7=AE=97=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/data.rs | 133 +++++--- .../fidc-core/src/platform_expr_strategy.rs | 307 ++++++------------ 2 files changed, 197 insertions(+), 243 deletions(-) diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index 6ec03a2..5eceec1 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -11,7 +11,6 @@ use crate::futures::FuturesTradingParameter; use crate::instrument::Instrument; use crate::risk_control::{ChinaAShareRiskControl, FidcRiskControlConfig}; - mod date_format { use chrono::NaiveDate; use serde::{self, Deserialize, Deserializer, Serializer}; @@ -575,6 +574,64 @@ impl AdjustedCloseSeries { )) } + fn decision_moving_average(&self, date: NaiveDate, lookback: usize) -> Option { + if lookback == 0 { + return None; + } + let end = match self.dates.binary_search(&date) { + Ok(index) => index, + Err(0) => return None, + Err(index) => index, + }; + if end < lookback { + return None; + } + let base_factor = self.backward_factors.get(end - 1).copied().flatten()?; + let start = end - lookback; + if self.missing_back_adjusted_close_prefix[end] + != self.missing_back_adjusted_close_prefix[start] + { + return None; + } + let sum = self.back_adjusted_close_prefix[end] - self.back_adjusted_close_prefix[start]; + if !sum.is_finite() { + return None; + } + Some(normalize_rolling_factor( + sum / lookback as f64 / base_factor, + 12, + )) + } + + fn values(&self, date: NaiveDate, lookback: usize, include_now: bool) -> Vec { + if lookback == 0 { + return Vec::new(); + } + let end = match self.dates.binary_search(&date) { + Ok(index) => index + usize::from(include_now), + Err(0) => return Vec::new(), + Err(index) => index, + }; + if end == 0 { + return Vec::new(); + } + let start = end.saturating_sub(lookback); + let Some(base_factor) = self.backward_factors.get(end - 1).copied().flatten() else { + return Vec::new(); + }; + self.back_adjusted_closes[start..end] + .iter() + .copied() + .collect::>>() + .map(|values| { + values + .into_iter() + .map(|value| normalize_rolling_factor(value / base_factor, 12)) + .collect() + }) + .unwrap_or_default() + } + fn latest_back_adjusted_close(&self, date: NaiveDate) -> Option { let index = match self.dates.binary_search(&date) { Ok(index) => index, @@ -641,7 +698,10 @@ impl SymbolPriceSeries { + if valid { *volume as f64 } else { 0.0 }, ); valid_volume_count_prefix.push( - valid_volume_count_prefix.last().copied().unwrap_or_default() + valid_volume_count_prefix + .last() + .copied() + .unwrap_or_default() + usize::from(valid), ); } @@ -781,40 +841,26 @@ impl SymbolPriceSeries { Some(sum / lookback as f64) } - fn decision_prev_close_values(&self, date: NaiveDate, lookback: usize) -> Option> { - if lookback == 0 { - return None; - } - let end = self.decision_end_index(date)?; - if end < lookback { - return None; - } - let start = end - lookback; - Some(self.prev_closes[start..end].to_vec()) - } - fn decision_volume_moving_average(&self, date: NaiveDate, lookback: usize) -> Option { let end = self.previous_completed_end_index(date)?; - self.valid_volume_window(end, lookback) - .map(|(start, end)| { - normalize_rolling_factor( - (self.valid_volume_sum_prefix[end] - self.valid_volume_sum_prefix[start]) - / lookback as f64, - 12, - ) - }) + self.valid_volume_window(end, lookback).map(|(start, end)| { + normalize_rolling_factor( + (self.valid_volume_sum_prefix[end] - self.valid_volume_sum_prefix[start]) + / lookback as f64, + 12, + ) + }) } fn current_volume_moving_average(&self, date: NaiveDate, lookback: usize) -> Option { let end = self.end_index(date)?; - self.valid_volume_window(end, lookback) - .map(|(start, end)| { - normalize_rolling_factor( - (self.valid_volume_sum_prefix[end] - self.valid_volume_sum_prefix[start]) - / lookback as f64, - 12, - ) - }) + self.valid_volume_window(end, lookback).map(|(start, end)| { + normalize_rolling_factor( + (self.valid_volume_sum_prefix[end] - self.valid_volume_sum_prefix[start]) + / lookback as f64, + 12, + ) + }) } fn decision_volume_values(&self, date: NaiveDate, lookback: usize) -> Option> { @@ -2409,8 +2455,8 @@ impl DataSet { let field = normalize_field(field); match field.as_str() { "close" | "prev_close" | "stock_close" | "price" => self - .market_series(symbol) - .and_then(|series| series.decision_close_moving_average(date, lookback)), + .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 @@ -2482,8 +2528,8 @@ impl DataSet { let field = normalize_field(field); match field.as_str() { "close" | "prev_close" | "stock_close" | "price" => self - .market_series(symbol) - .and_then(|series| series.decision_prev_close_values(date, lookback)) + .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) { @@ -2523,6 +2569,15 @@ impl DataSet { { return Vec::new(); } + if matches!( + field.as_str(), + "close" | "prev_close" | "stock_close" | "price" + ) { + return self + .adjusted_close_series(symbol) + .map(|series| series.values(date, lookback, true)) + .unwrap_or_default(); + } if matches!(field.as_str(), "volume" | "stock_volume") { return self .market_series(symbol) @@ -3500,10 +3555,8 @@ mod tests { .map(|(index, date)| { let mut extra_factors = BTreeMap::new(); if let Some(values) = availability { - extra_factors.insert( - "source_daily_volume_available".to_string(), - values[index], - ); + extra_factors + .insert("source_daily_volume_available".to_string(), values[index]); if values[index] >= 0.5 { extra_factors.insert("daily_volume".to_string(), volumes[index] as f64); } @@ -3674,6 +3727,10 @@ mod tests { data.market_current_numeric_moving_average(dates[2], "000001.SZ", "close", 3), Some(5.5) ); + assert_eq!( + data.market_decision_numeric_moving_average(dates[2], "000001.SZ", "close", 2), + Some(10.5) + ); assert_ne!( data.market_current_numeric_moving_average(dates[2], "000001.SZ", "close", 3), data.market_moving_average(dates[2], "000001.SZ", 3, PriceField::Close) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index c71034b..9d10248 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -390,7 +390,6 @@ pub struct PlatformExprStrategyConfig { pub matching_type: MatchingType, pub quote_quantity_limit: bool, pub current_day_precomputed_factors: bool, - pub prefer_precomputed_rolling_factors: bool, pub intraday_execution_time: Option, pub delayed_limit_open_exit_enabled: bool, pub delayed_limit_open_exit_time: Option, @@ -468,7 +467,6 @@ fn band_low(index_close) { matching_type: MatchingType::CurrentBarClose, quote_quantity_limit: true, current_day_precomputed_factors: false, - prefer_precomputed_rolling_factors: false, intraday_execution_time: None, delayed_limit_open_exit_enabled: false, delayed_limit_open_exit_time: None, @@ -724,73 +722,27 @@ struct PositionExpressionState { dividend_receivable: f64, } -fn precomputed_stock_rolling_mean( - extra_factors: &BTreeMap, - field: &str, - lookback: usize, -) -> Option { - if lookback == 0 { +fn framework_stock_rolling_factor_requirement(key: &str) -> Option<(&'static str, usize)> { + let key = key.trim().to_ascii_lowercase(); + let (field, raw_window) = if let Some(value) = key + .strip_prefix("ma") + .and_then(|value| value.strip_suffix("_prev_close")) + { + ("close", value) + } else if let Some(value) = key.strip_prefix("ma") { + ("close", value) + } else if let Some(value) = key.strip_prefix("vma") { + ("volume", value) + } else if let Some(value) = key.strip_prefix("avg_volume") { + ("volume", value) + } else { return None; - } - let value_for = |key: &str| { - extra_factors - .get(key) - .copied() - .filter(|value| value.is_finite()) }; - match field.trim().to_ascii_lowercase().as_str() { - "close" | "prev_close" | "stock_close" | "price" => { - let primary = format!("ma{lookback}_prev_close"); - let alias = format!("ma{lookback}"); - value_for(&primary).or_else(|| value_for(&alias)) - } - "volume" | "stock_volume" => { - let primary = format!("avg_volume{lookback}"); - let alias = format!("vma{lookback}"); - value_for(&primary).or_else(|| value_for(&alias)) - } - _ => None, - } -} - -fn precomputed_stock_current_rolling_mean( - extra_factors: &BTreeMap, - field: &str, - lookback: usize, -) -> Option { - if lookback == 0 { - return None; - } - let value_for = |key: &str| { - extra_factors - .get(key) - .copied() - .filter(|value| value.is_finite()) - }; - match field.trim().to_ascii_lowercase().as_str() { - "close" | "prev_close" | "stock_close" | "price" => { - // `rolling_mean_current("close", ...)` uses the framework's - // back-adjusted close series. Source Lake `maN_current_close` is - // calculated from raw close and is therefore not interchangeable. - value_for(&format!("ma{lookback}_current_back_adjusted_close")) - } - "volume" | "stock_volume" => value_for(&format!("avg_volume{lookback}_current")), - _ => None, - } -} - -fn is_precomputed_stock_current_rolling_key(key: &str) -> bool { - fn has_numeric_window(key: &str, prefix: &str, suffix: &str) -> bool { - key.strip_prefix(prefix) - .and_then(|value| value.strip_suffix(suffix)) - .is_some_and(|window| { - !window.is_empty() && window.bytes().all(|byte| byte.is_ascii_digit()) - }) - } - - has_numeric_window(key, "ma", "_current_close") - || has_numeric_window(key, "ma", "_current_back_adjusted_close") - || has_numeric_window(key, "avg_volume", "_current") + raw_window + .parse::() + .ok() + .filter(|window| *window > 0) + .map(|window| (field, window)) } struct SelectiveExpressionScope<'a> { @@ -3589,20 +3541,11 @@ impl PlatformExprStrategy { ctx: &StrategyContext<'_>, date: NaiveDate, symbol: &str, - extra_factors: &BTreeMap, field: &str, lookback: usize, ) -> Option { - let precomputed = precomputed_stock_rolling_mean(extra_factors, field, lookback); - let computed = || { - ctx.data - .market_decision_numeric_moving_average(date, symbol, field, lookback) - }; - if self.config.prefer_precomputed_rolling_factors { - precomputed.or_else(computed) - } else { - computed().or(precomputed) - } + ctx.data + .market_decision_numeric_moving_average(date, symbol, field, lookback) } fn stock_current_rolling_mean( @@ -3610,20 +3553,11 @@ impl PlatformExprStrategy { ctx: &StrategyContext<'_>, date: NaiveDate, symbol: &str, - extra_factors: &BTreeMap, field: &str, lookback: usize, ) -> Option { - let precomputed = precomputed_stock_current_rolling_mean(extra_factors, field, lookback); - let computed = || { - ctx.data - .market_current_numeric_moving_average(date, symbol, field, lookback) - }; - if self.config.prefer_precomputed_rolling_factors { - precomputed.or_else(computed) - } else { - computed().or(precomputed) - } + ctx.data + .market_current_numeric_moving_average(date, symbol, field, lookback) } fn stock_state_at_time( @@ -3732,15 +3666,8 @@ impl PlatformExprStrategy { if !self.stock_rolling_requirements.requires(field, lookback) { return f64::NAN; } - self.stock_decision_rolling_mean( - ctx, - date, - symbol, - &factor.extra_factors, - field, - lookback, - ) - .unwrap_or(f64::NAN) + self.stock_decision_rolling_mean(ctx, date, symbol, field, lookback) + .unwrap_or(f64::NAN) }; let stock_ma_short = rolling("close", self.config.stock_short_ma_days); let stock_ma_mid = rolling("close", self.config.stock_mid_ma_days); @@ -3858,20 +3785,7 @@ impl PlatformExprStrategy { }; let extra_factors = if self.stock_extra_factors_required { - let mut values = factor.extra_factors.clone(); - if date != factor_date { - values.retain(|key, _| !is_precomputed_stock_current_rolling_key(key)); - if let Some(current_factor) = ctx.data.factor(date, symbol) { - values.extend( - current_factor - .extra_factors - .iter() - .filter(|(key, _)| is_precomputed_stock_current_rolling_key(key)) - .map(|(key, value)| (key.clone(), *value)), - ); - } - } - values + factor.extra_factors.clone() } else { BTreeMap::new() }; @@ -5202,6 +5116,26 @@ impl PlatformExprStrategy { let key = Self::normalize_runtime_factor_key(&Self::parse_string_or_identifier( args.first().map(String::as_str).unwrap_or_default(), )?); + if let Some((field, lookback)) = framework_stock_rolling_factor_requirement(&key) { + let stock = stock.ok_or_else(|| { + BacktestError::Execution(format!( + "factor(\"{key}\") requires stock context" + )) + })?; + let value = self + .stock_decision_rolling_mean(ctx, day.date, &stock.symbol, field, lookback) + .ok_or_else(|| { + BacktestError::Execution(format!( + "missing framework rolling factor {key} for {} on {}", + stock.symbol, day.date + )) + })?; + return Ok(Self::push_runtime_helper_value( + scope, + scope_name, + Dynamic::from(value), + )); + } Ok(format!("factors[{}]", Self::quote_rhai_string(&key))) } "day_factor" => { @@ -5846,14 +5780,7 @@ impl PlatformExprStrategy { "rolling_mean(\"{other}\", {lookback}) requires stock context" )) })?; - self.stock_decision_rolling_mean( - ctx, - day.date, - &stock.symbol, - &stock.extra_factors, - other, - lookback, - ) + self.stock_decision_rolling_mean(ctx, day.date, &stock.symbol, other, lookback) } }; value.ok_or_else(|| { @@ -5895,14 +5822,7 @@ impl PlatformExprStrategy { "rolling_mean_current(\"{other}\", {lookback}) requires stock context" )) })?; - self.stock_current_rolling_mean( - ctx, - day.date, - &stock.symbol, - &stock.extra_factors, - other, - lookback, - ) + self.stock_current_rolling_mean(ctx, day.date, &stock.symbol, other, lookback) } }; value.ok_or_else(|| { @@ -11078,7 +10998,7 @@ mod tests { PlatformPortfolioDrawdownControlConfig, PlatformPortfolioDrawdownController, PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformStopTakeReferencePriceMode, PlatformTradeAction, PlatformUniverseActionKind, SelectionRiskDeferral, - StockFilterQuoteUsage, precomputed_stock_rolling_mean, + StockFilterQuoteUsage, framework_stock_rolling_factor_requirement, }; use crate::{ AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction, @@ -11125,6 +11045,27 @@ mod tests { ); } + #[test] + fn framework_rolling_factor_aliases_resolve_to_raw_market_series() { + assert_eq!( + framework_stock_rolling_factor_requirement("ma30"), + Some(("close", 30)) + ); + assert_eq!( + framework_stock_rolling_factor_requirement("ma30_prev_close"), + Some(("close", 30)) + ); + assert_eq!( + framework_stock_rolling_factor_requirement("vma100"), + Some(("volume", 100)) + ); + assert_eq!( + framework_stock_rolling_factor_requirement("avg_volume100"), + Some(("volume", 100)) + ); + assert_eq!(framework_stock_rolling_factor_requirement("alpha001"), None); + } + #[test] fn platform_rebalance_keeps_unresolved_delisted_position_without_orders_or_replacement() { let previous_date = d(2025, 1, 2); @@ -17114,10 +17055,10 @@ mod tests { } #[test] - fn platform_stock_expr_fast_path_handles_positive_volume_guard() { + fn platform_stock_expr_handles_positive_volume_guard() { let current = d(2023, 5, 4); let symbol = "000153.SZ"; - let build_data = |volume_ma5: f64, volume_ma100: f64| { + let build_data = |volume: u64| { DataSet::from_components( vec![Instrument { symbol: symbol.to_string(), @@ -17141,8 +17082,8 @@ mod tests { bid1: 9.99, ask1: 10.01, prev_close: 9.9, - volume: 1_000, - minute_volume: 1_000, + volume, + minute_volume: volume, bid1_volume: 2_000, ask1_volume: 2_000, trading_phase: Some("continuous".to_string()), @@ -17159,13 +17100,7 @@ mod tests { pe_ttm: 8.0, turnover_ratio: Some(1.0), effective_turnover_ratio: Some(1.0), - extra_factors: BTreeMap::from([ - ("ma5".to_string(), 11.0), - ("ma10".to_string(), 10.0), - ("ma30".to_string(), 9.0), - ("avg_volume5".to_string(), volume_ma5), - ("avg_volume100".to_string(), volume_ma100), - ]), + extra_factors: BTreeMap::new(), }], vec![CandidateEligibility { date: current, @@ -17193,12 +17128,10 @@ mod tests { }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.signal_symbol = symbol.to_string(); - cfg.prefer_precomputed_rolling_factors = true; - cfg.prelude = "let ma_ratio = 1.00001; let max_volume_ratio = 1;".to_string(); - cfg.stock_filter_expr = "rolling_mean(\"close\", 5) > rolling_mean(\"close\", 10) * ma_ratio && rolling_mean(\"close\", 10) > rolling_mean(\"close\", 30) * ma_ratio && rolling_mean(\"volume\", 5) < rolling_mean(\"volume\", 100) * max_volume_ratio && rolling_mean(\"volume\", 5) > 0 && rolling_mean(\"volume\", 100) > 0".to_string(); + cfg.stock_filter_expr = "volume > 0".to_string(); - for (volume_ma5, volume_ma100, expected) in [(50.0, 100.0, true), (0.0, 100.0, false)] { - let data = build_data(volume_ma5, volume_ma100); + for (volume, expected) in [(50, true), (0, false)] { + let data = build_data(volume); let portfolio = PortfolioState::new(1_000_000.0); let subscriptions = BTreeSet::new(); let ctx = StrategyContext { @@ -17229,7 +17162,6 @@ mod tests { .expect("stock expr"), expected ); - assert_eq!(strategy.ast_cache_misses(), 0); } } @@ -21985,7 +21917,7 @@ mod tests { end_time_expr: None, when_expr: Some( concat!( - "ma(\"close\", 2) == 11.5", + "ma(\"close\", 2) == 10.7", " && vma(2) == 150.0", " && rolling_mean_current(\"close\", 2) == 11.7", " && rolling_mean_current(\"volume\", 2) == 250.0", @@ -21993,8 +21925,8 @@ mod tests { " && rolling_return_stddev_current(\"close\", 2) > 0.006", " && rolling_return_stddev_current(\"close\", 2) < 0.007", " && rolling_sum(\"volume\", 2) == 300.0", - " && rolling_min(\"close\", 2) == 11.0", - " && rolling_max(\"close\", 2) == 12.0", + " && rolling_min(\"close\", 2) == 10.2", + " && rolling_max(\"close\", 2) == 11.2", " && stddev(\"close\", 2) > 0.49", " && rolling_zscore(\"close\", 2) > 0.9", " && pct_change(\"close\", 1) > 0.09", @@ -22054,7 +21986,7 @@ mod tests { pe_ttm: 8.0, turnover_ratio: Some(1.0), effective_turnover_ratio: Some(1.0), - extra_factors: BTreeMap::new(), + extra_factors: BTreeMap::from([("adjustment_factor_backward1".to_string(), 1.0)]), }) .collect::>(); let candidate_rows = dates @@ -28937,7 +28869,7 @@ mod tests { } #[test] - fn platform_stock_state_can_prefer_precomputed_rolling_factors() { + fn platform_stock_state_uses_framework_rolling_series() { let dates = [ d(2025, 1, 2), d(2025, 1, 3), @@ -28949,12 +28881,6 @@ mod tests { let date = dates[5]; let symbol = "300001.SZ"; let mut extra_factors = BTreeMap::new(); - extra_factors.insert("ma5_prev_close".to_string(), 99.0); - extra_factors.insert("ma10_prev_close".to_string(), 98.0); - extra_factors.insert("ma30_prev_close".to_string(), 97.0); - extra_factors.insert("avg_volume5".to_string(), 88.0); - extra_factors.insert("avg_volume100".to_string(), 99.0); - extra_factors.insert("ma5_current_close".to_string(), 999.0); extra_factors.insert("adjustment_factor_backward1".to_string(), 1.0); let data = DataSet::from_components( vec![Instrument { @@ -29052,36 +28978,36 @@ mod tests { }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.signal_symbol = symbol.to_string(); - cfg.prefer_precomputed_rolling_factors = true; - cfg.stock_filter_expr = "rolling_mean(\"close\", 5) > rolling_mean(\"close\", 10) && rolling_mean(\"close\", 10) > rolling_mean(\"close\", 30) && rolling_mean(\"volume\", 5) < rolling_mean(\"volume\", 100)".to_string(); + cfg.stock_filter_expr = + "rolling_mean(\"close\", 5) == 10.0 && rolling_mean(\"volume\", 5) == 1000.0" + .to_string(); let strategy = PlatformExprStrategy::new(cfg); let stock = strategy .stock_state_with_factor_date(&ctx, date, date, symbol) .expect("stock state"); - assert_eq!(stock.stock_ma5, 99.0); - assert_eq!(stock.stock_volume_ma5, 88.0); + assert_eq!(stock.stock_ma5, 10.0); + assert_eq!(stock.stock_volume_ma5, 1_000.0); let day = strategy.day_state(&ctx, date).expect("day state"); assert!( strategy .stock_passes_expr(&ctx, &day, &stock) - .expect("precomputed decision rolling filter") + .expect("framework decision rolling filter") ); assert_eq!( strategy .resolve_rolling_mean(&ctx, &day, Some(&stock), "close", 5) - .expect("precomputed decision close rolling mean"), - 99.0 + .expect("framework decision close rolling mean"), + 10.0 ); assert_eq!( strategy .resolve_rolling_mean(&ctx, &day, Some(&stock), "volume", 5) - .expect("precomputed decision volume rolling mean"), - 88.0 + .expect("framework decision volume rolling mean"), + 1_000.0 ); let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.signal_symbol = symbol.to_string(); - cfg.prefer_precomputed_rolling_factors = true; cfg.stock_filter_expr = "rolling_mean_current(\"close\", 5) == 10.0 && rolling_mean_current(\"volume\", 5) == 1000.0".to_string(); let strategy = PlatformExprStrategy::new(cfg); let stock = strategy @@ -29221,8 +29147,7 @@ mod tests { }; let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.signal_symbol = symbol.to_string(); - cfg.prefer_precomputed_rolling_factors = true; - cfg.stock_filter_expr = "rolling_mean_current(\"close\", 5) == 20.0 && rolling_mean_current(\"volume\", 5) == 2000.0".to_string(); + cfg.stock_filter_expr = "rolling_mean_current(\"close\", 1) == 10.0 && rolling_mean_current(\"volume\", 1) == 1000.0".to_string(); let strategy = PlatformExprStrategy::new(cfg); let stock = strategy .stock_state_with_factor_date(&ctx, decision_date, factor_date, symbol) @@ -29236,20 +29161,20 @@ mod tests { ); assert_eq!( strategy - .resolve_current_rolling_mean(&ctx, &day, Some(&stock), "close", 5) + .resolve_current_rolling_mean(&ctx, &day, Some(&stock), "close", 1) .expect("current close rolling mean"), - 20.0 + 10.0 ); assert_eq!( strategy - .resolve_current_rolling_mean(&ctx, &day, Some(&stock), "volume", 5) + .resolve_current_rolling_mean(&ctx, &day, Some(&stock), "volume", 1) .expect("current volume rolling mean"), - 2_000.0 + 1_000.0 ); } #[test] - fn platform_stock_state_falls_back_when_precomputed_rolling_is_missing() { + fn platform_stock_state_uses_market_series_when_factor_map_has_no_rolling() { let current = d(2025, 5, 30); let start = current - chrono::Duration::days(100); let symbol = "300022.SZ"; @@ -29350,7 +29275,6 @@ mod tests { let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); cfg.signal_symbol = symbol.to_string(); - cfg.prefer_precomputed_rolling_factors = true; cfg.stock_filter_expr = "rolling_mean(\"volume\", 100) > 0".to_string(); let strategy = PlatformExprStrategy::new(cfg.clone()); let stock = strategy @@ -29364,7 +29288,6 @@ mod tests { .expect("stock expr") ); - cfg.prefer_precomputed_rolling_factors = false; let strategy = PlatformExprStrategy::new(cfg); let stock = strategy .stock_state_with_factor_date(&ctx, current, current, symbol) @@ -29373,41 +29296,15 @@ mod tests { } #[test] - fn precomputed_rolling_mean_ignores_strategy_specific_v104_labels() { - let mut extra_factors = BTreeMap::new(); - extra_factors.insert("sf_jq_v104_ma5".to_string(), 99.0); - extra_factors.insert("sf_jq_v104_v100".to_string(), 88.0); - + fn strategy_specific_labels_are_not_framework_rolling_factors() { assert_eq!( - precomputed_stock_rolling_mean(&extra_factors, "close", 5), + framework_stock_rolling_factor_requirement("sf_jq_v104_ma5"), None ); assert_eq!( - precomputed_stock_rolling_mean(&extra_factors, "volume", 100), + framework_stock_rolling_factor_requirement("sf_jq_v104_v100"), None ); - - extra_factors.insert("ma5_prev_close".to_string(), 10.5); - extra_factors.insert("ma40_prev_close".to_string(), 11.5); - extra_factors.insert("avg_volume100".to_string(), 120_000.0); - extra_factors.insert("avg_volume40".to_string(), 40_000.0); - - assert_eq!( - precomputed_stock_rolling_mean(&extra_factors, "close", 5), - Some(10.5) - ); - assert_eq!( - precomputed_stock_rolling_mean(&extra_factors, "close", 40), - Some(11.5) - ); - assert_eq!( - precomputed_stock_rolling_mean(&extra_factors, "volume", 100), - Some(120_000.0) - ); - assert_eq!( - precomputed_stock_rolling_mean(&extra_factors, "volume", 40), - Some(40_000.0) - ); } #[test]