fix: evaluate buy quote conditions at the active schedule clock
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user