diff --git a/crates/fidc-core/src/engine.rs b/crates/fidc-core/src/engine.rs index ca2fe8c..6a9df02 100644 --- a/crates/fidc-core/src/engine.rs +++ b/crates/fidc-core/src/engine.rs @@ -2519,7 +2519,7 @@ where )?; let on_day_open_orders = self.open_order_views(); let decision_quote_times = self.strategy.decision_quote_times(); - if !decision_quote_times.is_empty() { + if self.execution_quote_loader.is_some() && !decision_quote_times.is_empty() { let decision_quote_symbols = self.strategy.decision_quote_symbols(&StrategyContext { execution_date, diff --git a/crates/fidc-core/tests/decision_quote_preload.rs b/crates/fidc-core/tests/decision_quote_preload.rs index b63d739..e0b4202 100644 --- a/crates/fidc-core/tests/decision_quote_preload.rs +++ b/crates/fidc-core/tests/decision_quote_preload.rs @@ -62,6 +62,78 @@ impl Strategy for DecisionQuoteReader { } } +struct NoLoaderDecisionQuoteStrategy { + symbol_plan_calls: Arc>, +} + +impl Strategy for NoLoaderDecisionQuoteStrategy { + fn name(&self) -> &str { + "no_loader_decision_quote_strategy" + } + + fn decision_quote_times(&self) -> Vec { + vec![t(10, 18, 0)] + } + + fn decision_quote_symbols( + &mut self, + _ctx: &StrategyContext<'_>, + ) -> Result, fidc_core::BacktestError> { + *self + .symbol_plan_calls + .lock() + .expect("symbol plan counter mutex") += 1; + Ok(std::collections::BTreeSet::new()) + } +} + +#[test] +fn engine_skips_decision_quote_symbol_plan_without_loader() { + let date = d(2026, 1, 5); + let data = DataSet::from_components( + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + vec![BenchmarkSnapshot { + date, + benchmark: "000852.SH".to_string(), + open: 1000.0, + close: 1001.0, + prev_close: 999.0, + volume: 1_000_000, + }], + ) + .expect("dataset"); + let broker = BrokerSimulator::new_with_execution_price( + ChinaAShareCostModel::default(), + ChinaEquityRuleHooks, + PriceField::Close, + ) + .with_matching_type(MatchingType::CurrentBarClose); + let config = BacktestConfig { + initial_cash: 10_000.0, + benchmark_code: "000852.SH".to_string(), + start_date: Some(date), + end_date: Some(date), + decision_lag_trading_days: 0, + execution_price_field: PriceField::Close, + }; + let symbol_plan_calls = Arc::new(Mutex::new(0usize)); + let strategy = NoLoaderDecisionQuoteStrategy { + symbol_plan_calls: Arc::clone(&symbol_plan_calls), + }; + let mut engine = BacktestEngine::new(data, strategy, broker, config); + + engine.run().expect("backtest should run"); + + assert_eq!( + *symbol_plan_calls.lock().expect("symbol plan counter mutex"), + 0, + "a preloaded/no-loader engine cannot use a newly computed quote symbol plan" + ); +} + #[test] fn engine_preloads_declared_decision_quotes_for_current_positions() { let first = d(2026, 1, 5);