From d0639558b30611d92981ef914cfaec930e8f316e Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 27 Aug 2026 10:14:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=81=A2=E5=A4=8DSource=20Lake=E6=BB=9A?= =?UTF-8?q?=E5=8A=A8=E5=9B=A0=E5=AD=90=E8=BF=90=E8=A1=8C=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 137 ++++++++++++++++-- 1 file changed, 126 insertions(+), 11 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 0e4272d..7bb14df 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -9,8 +9,8 @@ use rhai::{AST, Dynamic, Engine, Map, Scope}; use crate::broker::{MatchingType, RebalanceCashMode, SlippageModel}; use crate::cost::ChinaAShareCostModel; use crate::data::{ - DailyMarketSnapshot, EligibleUniverseSnapshot, PriceField, decision_free_float_cap_bn, - decision_market_cap_bn, + DailyMarketSnapshot, EligibleUniverseSnapshot, NumericFactorMap, PriceField, + decision_free_float_cap_bn, decision_market_cap_bn, }; use crate::engine::BacktestError; use crate::events::OrderSide; @@ -424,6 +424,11 @@ pub struct PlatformExprStrategyConfig { pub matching_type: MatchingType, pub quote_quantity_limit: bool, pub current_day_precomputed_factors: bool, + /// Use audited Strategy Factory Source Lake rolling fields when the + /// runtime intentionally loads only the latest decision rows. This is + /// opt-in; ordinary backtests always recompute rolling values from the + /// canonical market series. + 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, @@ -495,6 +500,7 @@ impl PlatformExprStrategyConfig { 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, @@ -802,6 +808,42 @@ fn framework_stock_rolling_factor_requirement(key: &str) -> Option<(&'static str .map(|window| (field, window)) } +fn precomputed_stock_rolling_mean(get: F, field: &str, lookback: usize) -> Option +where + F: Fn(&str) -> Option, +{ + if lookback == 0 { + return None; + } + let value_for = |key: String| get(&key).filter(|value| value.is_finite()); + match field.trim().to_ascii_lowercase().as_str() { + "close" | "prev_close" | "stock_close" | "price" => { + value_for(format!("ma{lookback}_prev_close")) + .or_else(|| value_for(format!("ma{lookback}"))) + } + "volume" | "stock_volume" => value_for(format!("avg_volume{lookback}")) + .or_else(|| value_for(format!("vma{lookback}"))), + _ => None, + } +} + +fn precomputed_stock_current_rolling_mean(get: F, field: &str, lookback: usize) -> Option +where + F: Fn(&str) -> Option, +{ + if lookback == 0 { + return None; + } + let value_for = |key: String| get(&key).filter(|value| value.is_finite()); + match field.trim().to_ascii_lowercase().as_str() { + "close" | "prev_close" | "stock_close" | "price" => { + value_for(format!("ma{lookback}_current_back_adjusted_close")) + } + "volume" | "stock_volume" => value_for(format!("avg_volume{lookback}_current")), + _ => None, + } +} + struct SelectiveExpressionScope<'a> { inner: Scope<'static>, required_identifiers: &'a AHashSet, @@ -3775,13 +3817,24 @@ impl PlatformExprStrategy { date: NaiveDate, symbol_id: u32, symbol: &str, + extra_factors: Option<&NumericFactorMap>, field: &str, lookback: usize, ) -> Option { - ctx.data - .market_decision_numeric_moving_average_by_symbol_id( - date, symbol_id, symbol, field, lookback, - ) + let computed = || { + ctx.data + .market_decision_numeric_moving_average_by_symbol_id( + date, symbol_id, symbol, field, lookback, + ) + }; + let precomputed = extra_factors.and_then(|factors| { + precomputed_stock_rolling_mean(|key| factors.get(key).copied(), field, lookback) + }); + if self.config.prefer_precomputed_rolling_factors { + precomputed.or_else(computed) + } else { + computed().or(precomputed) + } } fn stock_current_rolling_mean( @@ -3790,12 +3843,23 @@ impl PlatformExprStrategy { date: NaiveDate, symbol_id: u32, symbol: &str, + extra_factors: Option<&NumericFactorMap>, field: &str, lookback: usize, ) -> Option { - ctx.data.market_current_numeric_moving_average_by_symbol_id( - date, symbol_id, symbol, field, lookback, - ) + let computed = || { + ctx.data.market_current_numeric_moving_average_by_symbol_id( + date, symbol_id, symbol, field, lookback, + ) + }; + let precomputed = extra_factors.and_then(|factors| { + precomputed_stock_current_rolling_mean(|key| factors.get(key).copied(), field, lookback) + }); + if self.config.prefer_precomputed_rolling_factors { + precomputed.or_else(computed) + } else { + computed().or(precomputed) + } } fn stock_state_at_time( @@ -3920,8 +3984,16 @@ impl PlatformExprStrategy { if !self.stock_rolling_requirements.requires(field, lookback) { return f64::NAN; } - self.stock_decision_rolling_mean(ctx, date, symbol_id, symbol, field, lookback) - .unwrap_or(f64::NAN) + self.stock_decision_rolling_mean( + ctx, + date, + symbol_id, + symbol, + Some(&factor.extra_factors), + 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); @@ -6037,6 +6109,9 @@ impl PlatformExprStrategy { day.date, stock.symbol_id, &stock.symbol, + ctx.data + .factor_by_symbol_id(day.date, stock.symbol_id) + .map(|factor| &factor.extra_factors), field, lookback, ) @@ -6692,6 +6767,9 @@ impl PlatformExprStrategy { day.date, stock.symbol_id, &stock.symbol, + ctx.data + .factor_by_symbol_id(day.date, stock.symbol_id) + .map(|factor| &factor.extra_factors), other, lookback, ) @@ -6741,6 +6819,9 @@ impl PlatformExprStrategy { day.date, stock.symbol_id, &stock.symbol, + ctx.data + .factor_by_symbol_id(day.date, stock.symbol_id) + .map(|factor| &factor.extra_factors), other, lookback, ) @@ -12092,6 +12173,7 @@ mod tests { PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformStopTakeReferencePriceMode, PlatformTradeAction, PlatformUniverseActionKind, RuntimeHelperResolution, SelectionRiskDeferral, StockFilterQuoteUsage, framework_stock_rolling_factor_requirement, + precomputed_stock_current_rolling_mean, precomputed_stock_rolling_mean, }; use crate::{ AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction, @@ -12159,6 +12241,39 @@ mod tests { assert_eq!(framework_stock_rolling_factor_requirement("alpha001"), None); } + #[test] + fn source_lake_precomputed_rolling_aliases_use_only_canonical_fields() { + let factors = BTreeMap::from([ + ("ma5_prev_close".to_string(), 11.0), + ("ma5".to_string(), 12.0), + ("ma5_current_close".to_string(), 99.0), + ("ma5_current_back_adjusted_close".to_string(), 13.0), + ("avg_volume5".to_string(), 100.0), + ("avg_volume5_current".to_string(), 120.0), + ]); + let get = |key: &str| factors.get(key).copied(); + assert_eq!(precomputed_stock_rolling_mean(get, "close", 5), Some(11.0)); + assert_eq!( + precomputed_stock_current_rolling_mean(get, "close", 5), + Some(13.0) + ); + assert_eq!( + precomputed_stock_rolling_mean(get, "volume", 5), + Some(100.0) + ); + assert_eq!( + precomputed_stock_current_rolling_mean(get, "volume", 5), + Some(120.0) + ); + // A source row that only has a raw current-close alias must not be + // mistaken for the adjusted rolling value. + let raw_only = BTreeMap::from([("ma5_current_close".to_string(), 99.0)]); + assert_eq!( + precomputed_stock_current_rolling_mean(|key| raw_only.get(key).copied(), "close", 5,), + None + ); + } + #[test] fn typed_runtime_numbers_preserve_legacy_rhai_formatting() { let RuntimeHelperResolution::Number(value) =