fix: evaluate buy quote conditions at the active schedule clock

This commit is contained in:
boris
2026-09-08 01:45:39 +08:00
parent 30da6eaead
commit 326438aac9
+38 -2
View File
@@ -12212,17 +12212,19 @@ impl PlatformExprStrategy {
} }
let day = self.day_state(ctx, ctx.decision_date)?; let day = self.day_state(ctx, ctx.decision_date)?;
let (market_date, _, factor_date) = self.selection_dates(ctx); 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)) let needs_quote = Self::stock_filter_quote_usage_for_expr(&Self::normalize_expr(&self.config.buy_filter_expr))
!= StockFilterQuoteUsage::DailyOnly; != StockFilterQuoteUsage::DailyOnly;
for symbol in symbols { for symbol in symbols {
if needs_quote && self.uses_intraday_execution_quotes() && !ctx.is_lagged_execution() 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!( return Err(BacktestError::Execution(format!(
"buy condition quote unavailable: symbol={symbol} decision_date={}", ctx.decision_date, "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)? { if !self.eval_bool(ctx, &self.config.buy_filter_expr, &day, Some(&stock), None)? {
decision.buy_denials.insert(symbol, format!( decision.buy_denials.insert(symbol, format!(
"strategy_buy_condition_false decision_date={} expression={}", "strategy_buy_condition_false decision_date={} expression={}",
@@ -14073,6 +14075,40 @@ mod tests {
assert_eq!(strategy.selection_quote_usage, StockFilterQuoteUsage::DailyOnly); 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] #[test]
fn completed_session_factor_dates_exclude_intraday_and_preserve_next_open() { fn completed_session_factor_dates_exclude_intraday_and_preserve_next_open() {
let prev = d(2025, 1, 2); let prev = d(2025, 1, 2);