diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 17cc4df..fe2d61b 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -218,6 +218,7 @@ 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, @@ -286,6 +287,7 @@ fn band_low(index_close) { matching_type: MatchingType::NextTickLast, 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, @@ -2090,6 +2092,26 @@ impl PlatformExprStrategy { self.stock_state_with_factor_date_and_time(ctx, date, factor_date, symbol, None) } + fn stock_decision_rolling_mean( + &self, + 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(computed) + } else { + computed.or(precomputed) + } + } + fn stock_state_at_time( &self, ctx: &StrategyContext<'_>, @@ -2116,78 +2138,59 @@ impl PlatformExprStrategy { let factor = ctx.data.require_factor(factor_date, symbol)?; let candidate = ctx.data.require_candidate(date, symbol)?; let instrument = ctx.data.instrument(symbol); - let stock_ma_short = ctx - .data - .market_decision_close_moving_average(date, symbol, self.config.stock_short_ma_days) - .or_else(|| { - precomputed_stock_rolling_mean( - &factor.extra_factors, - "close", - self.config.stock_short_ma_days, - ) - }) + let stock_ma_short = self + .stock_decision_rolling_mean( + ctx, + date, + symbol, + &factor.extra_factors, + "close", + self.config.stock_short_ma_days, + ) .unwrap_or(f64::NAN); - let stock_ma_mid = ctx - .data - .market_decision_close_moving_average(date, symbol, self.config.stock_mid_ma_days) - .or_else(|| { - precomputed_stock_rolling_mean( - &factor.extra_factors, - "close", - self.config.stock_mid_ma_days, - ) - }) + let stock_ma_mid = self + .stock_decision_rolling_mean( + ctx, + date, + symbol, + &factor.extra_factors, + "close", + self.config.stock_mid_ma_days, + ) .unwrap_or(f64::NAN); - let stock_ma_long = ctx - .data - .market_decision_close_moving_average(date, symbol, self.config.stock_long_ma_days) - .or_else(|| { - precomputed_stock_rolling_mean( - &factor.extra_factors, - "close", - self.config.stock_long_ma_days, - ) - }) + let stock_ma_long = self + .stock_decision_rolling_mean( + ctx, + date, + symbol, + &factor.extra_factors, + "close", + self.config.stock_long_ma_days, + ) .unwrap_or(f64::NAN); - let stock_ma5 = ctx - .data - .market_decision_close_moving_average(date, symbol, 5) - .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 5)) + let stock_ma5 = self + .stock_decision_rolling_mean(ctx, date, symbol, &factor.extra_factors, "close", 5) .unwrap_or(f64::NAN); - let stock_ma10 = ctx - .data - .market_decision_close_moving_average(date, symbol, 10) - .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 10)) + let stock_ma10 = self + .stock_decision_rolling_mean(ctx, date, symbol, &factor.extra_factors, "close", 10) .unwrap_or(f64::NAN); - let stock_ma20 = ctx - .data - .market_decision_close_moving_average(date, symbol, 20) - .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 20)) + let stock_ma20 = self + .stock_decision_rolling_mean(ctx, date, symbol, &factor.extra_factors, "close", 20) .unwrap_or(f64::NAN); - let stock_ma30 = ctx - .data - .market_decision_close_moving_average(date, symbol, 30) - .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 30)) + let stock_ma30 = self + .stock_decision_rolling_mean(ctx, date, symbol, &factor.extra_factors, "close", 30) .unwrap_or(f64::NAN); - let stock_volume_ma5 = ctx - .data - .market_decision_volume_moving_average(date, symbol, 5) - .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 5)) + let stock_volume_ma5 = self + .stock_decision_rolling_mean(ctx, date, symbol, &factor.extra_factors, "volume", 5) .unwrap_or(f64::NAN); - let stock_volume_ma10 = ctx - .data - .market_decision_volume_moving_average(date, symbol, 10) - .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 10)) + let stock_volume_ma10 = self + .stock_decision_rolling_mean(ctx, date, symbol, &factor.extra_factors, "volume", 10) .unwrap_or(f64::NAN); - let stock_volume_ma20 = ctx - .data - .market_decision_volume_moving_average(date, symbol, 20) - .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 20)) + let stock_volume_ma20 = self + .stock_decision_rolling_mean(ctx, date, symbol, &factor.extra_factors, "volume", 20) .unwrap_or(f64::NAN); - let stock_volume_ma60 = ctx - .data - .market_decision_volume_moving_average(date, symbol, 60) - .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 60)) + let stock_volume_ma60 = self + .stock_decision_rolling_mean(ctx, date, symbol, &factor.extra_factors, "volume", 60) .unwrap_or(f64::NAN); let touched_upper_limit = !market.paused && (market.is_at_upper_limit_price(market.close) @@ -4023,16 +4026,14 @@ impl PlatformExprStrategy { "rolling_mean(\"{other}\", {lookback}) requires stock context" )) })?; - ctx.data - .market_decision_numeric_moving_average( - day.date, - &stock.symbol, - other, - lookback, - ) - .or_else(|| { - precomputed_stock_rolling_mean(&stock.extra_factors, other, lookback) - }) + self.stock_decision_rolling_mean( + ctx, + day.date, + &stock.symbol, + &stock.extra_factors, + other, + lookback, + ) } }; value.ok_or_else(|| { @@ -14697,6 +14698,123 @@ mod tests { )); } + #[test] + fn platform_stock_state_can_prefer_precomputed_rolling_factors() { + let dates = [ + d(2025, 1, 2), + d(2025, 1, 3), + d(2025, 1, 6), + d(2025, 1, 7), + d(2025, 1, 8), + d(2025, 1, 9), + ]; + 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("avg_volume5".to_string(), 88.0); + let data = DataSet::from_components( + vec![Instrument { + symbol: symbol.to_string(), + name: symbol.to_string(), + board: "SZ".to_string(), + round_lot: 100, + listed_at: Some(d(2020, 1, 1)), + delisted_at: None, + status: "active".to_string(), + }], + dates + .into_iter() + .map(|trade_date| DailyMarketSnapshot { + date: trade_date, + symbol: symbol.to_string(), + timestamp: None, + day_open: 10.0, + open: 10.0, + high: 10.2, + low: 9.8, + close: 10.0, + last_price: 10.0, + bid1: 9.99, + ask1: 10.01, + prev_close: 10.0, + volume: 1_000, + tick_volume: 1_000, + bid1_volume: 1_000, + ask1_volume: 1_000, + trading_phase: None, + paused: false, + upper_limit: 11.0, + lower_limit: 9.0, + price_tick: 0.01, + }) + .collect(), + vec![DailyFactorSnapshot { + date, + symbol: symbol.to_string(), + market_cap_bn: 20.0, + free_float_cap_bn: 20.0, + pe_ttm: 0.0, + turnover_ratio: None, + effective_turnover_ratio: None, + extra_factors, + }], + vec![CandidateEligibility { + date, + symbol: symbol.to_string(), + is_st: false, + is_new_listing: false, + is_paused: false, + allow_buy: true, + allow_sell: true, + is_kcb: false, + is_one_yuan: false, + }], + vec![BenchmarkSnapshot { + date, + benchmark: "000852.SH".to_string(), + open: 1000.0, + close: 1002.0, + prev_close: 998.0, + volume: 1_000_000, + }], + ) + .expect("dataset"); + let portfolio = PortfolioState::new(100_000.0); + let subscriptions = BTreeSet::new(); + let ctx = StrategyContext { + execution_date: date, + decision_date: date, + decision_index: 5, + data: &data, + portfolio: &portfolio, + futures_account: None, + open_orders: &[], + dynamic_universe: None, + subscriptions: &subscriptions, + process_events: &[], + active_process_event: None, + active_datetime: None, + order_events: &[], + fills: &[], + }; + let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.prefer_precomputed_rolling_factors = true; + 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); + + let strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); + let stock = strategy + .stock_state_with_factor_date(&ctx, date, date, symbol) + .expect("stock state"); + assert_eq!(stock.stock_ma5, 10.0); + assert_eq!(stock.stock_volume_ma5, 1_000.0); + } + #[test] fn platform_strategy_emits_target_shares_explicit_action() { let date = d(2025, 2, 3);