diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 071c5e6..21000e2 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -12212,17 +12212,19 @@ impl PlatformExprStrategy { } let day = self.day_state(ctx, ctx.decision_date)?; let (market_date, _, factor_date) = self.selection_dates(ctx); + let execution_time = ctx.active_datetime.filter(|value| value.date() == market_date) + .map(|value| value.time()).or(self.config.intraday_execution_time); let needs_quote = Self::stock_filter_quote_usage_for_expr(&Self::normalize_expr(&self.config.buy_filter_expr)) != StockFilterQuoteUsage::DailyOnly; for symbol in symbols { if needs_quote && self.uses_intraday_execution_quotes() && !ctx.is_lagged_execution() - && self.scheduled_quote(ctx, market_date, &symbol).is_none() + && self.scheduled_quote_at_time(ctx, market_date, &symbol, execution_time).is_none() { return Err(BacktestError::Execution(format!( "buy condition quote unavailable: symbol={symbol} decision_date={}", ctx.decision_date, ))); } - let stock = self.stock_state_with_factor_date(ctx, market_date, factor_date, &symbol)?; + let stock = self.stock_state_with_factor_date_and_time(ctx, market_date, factor_date, &symbol, execution_time, true)?; if !self.eval_bool(ctx, &self.config.buy_filter_expr, &day, Some(&stock), None)? { decision.buy_denials.insert(symbol, format!( "strategy_buy_condition_false decision_date={} expression={}", @@ -14073,6 +14075,40 @@ mod tests { assert_eq!(strategy.selection_quote_usage, StockFilterQuoteUsage::DailyOnly); } + #[test] + fn buy_filter_uses_active_schedule_time_instead_of_first_configured_time() { + let date = d(2025, 1, 2); + let symbol = "000001.SZ"; + let parts = single_symbol_platform_data(&[date], symbol).snapshot_components(); + let quotes = [(10, 18, 9.5), (14, 59, 10.5)].into_iter().map(|(hour, minute, price)| IntradayExecutionQuote { + date, symbol: symbol.to_string(), timestamp: date.and_hms_opt(hour, minute, 0).unwrap(), + last_price: price, bid1: price, ask1: price, bid1_volume: 1000, ask1_volume: 1000, + volume_delta: 1000, amount_delta: price * 1000.0, trading_phase: Some("continuous".to_string()), + }).collect(); + let data = DataSet::from_components_with_actions_and_quotes(parts.instruments, parts.market, + parts.factors, parts.candidates, parts.benchmarks, Vec::new(), quotes).unwrap(); + let portfolio = PortfolioState::new(30_000.0); + let subscriptions = BTreeSet::new(); + let mut ctx = StrategyContext { + execution_date: date, decision_date: date, decision_index: 0, 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::generic(); + cfg.signal_symbol = symbol.to_string(); + cfg.buy_filter_expr = "last > 10".to_string(); + cfg.intraday_execution_time = NaiveTime::from_hms_opt(10, 18, 0); + let strategy = PlatformExprStrategy::new(cfg); + for (hour, minute, denied) in [(10, 18, true), (14, 59, false)] { + ctx.active_datetime = Some(date.and_hms_opt(hour, minute, 0).unwrap()); + let mut decision = crate::StrategyDecision::default(); + decision.order_intents.push(OrderIntent::TargetValue { symbol: symbol.to_string(), target_value: 10_000.0, reason: "target".to_string() }); + strategy.attach_buy_denials(&ctx, &mut decision).unwrap(); + assert_eq!(decision.buy_denials.contains_key(symbol), denied); + } + } + #[test] fn completed_session_factor_dates_exclude_intraday_and_preserve_next_open() { let prev = d(2025, 1, 2);