fix(engine): resolve decision quote scope from the actual run context

This commit is contained in:
boris
2026-09-13 11:18:38 +08:00
committed by boris
parent d15abc18ae
commit b1ca2dfada
2 changed files with 17 additions and 48 deletions
+8 -33
View File
@@ -473,7 +473,6 @@ pub struct BacktestEngine<S, C, R> {
futures_cost_model: FuturesTransactionCostModel, futures_cost_model: FuturesTransactionCostModel,
futures_validation_config: FuturesValidationConfig, futures_validation_config: FuturesValidationConfig,
execution_quote_loader: Option<ExecutionQuoteLoader>, execution_quote_loader: Option<ExecutionQuoteLoader>,
preplanned_decision_quote_symbols_by_date: Option<Arc<BTreeMap<NaiveDate, BTreeSet<String>>>>,
execution_quote_request_cache: execution_quote_request_cache:
BTreeSet<(NaiveDate, String, Option<NaiveTime>, Option<NaiveTime>)>, BTreeSet<(NaiveDate, String, Option<NaiveTime>, Option<NaiveTime>)>,
execution_absence_notes: BTreeMap<NaiveDate, Vec<String>>, execution_absence_notes: BTreeMap<NaiveDate, Vec<String>>,
@@ -574,7 +573,6 @@ impl<S, C, R> BacktestEngine<S, C, R> {
futures_cost_model: FuturesTransactionCostModel::default(), futures_cost_model: FuturesTransactionCostModel::default(),
futures_validation_config: FuturesValidationConfig::default(), futures_validation_config: FuturesValidationConfig::default(),
execution_quote_loader: None, execution_quote_loader: None,
preplanned_decision_quote_symbols_by_date: None,
execution_quote_request_cache: BTreeSet::new(), execution_quote_request_cache: BTreeSet::new(),
execution_absence_notes: BTreeMap::new(), execution_absence_notes: BTreeMap::new(),
execution_lifecycle_reported: BTreeSet::new(), execution_lifecycle_reported: BTreeSet::new(),
@@ -601,14 +599,6 @@ impl<S, C, R> BacktestEngine<S, C, R> {
self self
} }
pub fn with_preplanned_decision_quote_symbols_by_date(
mut self,
symbols_by_date: Arc<BTreeMap<NaiveDate, BTreeSet<String>>>,
) -> Self {
self.preplanned_decision_quote_symbols_by_date = Some(symbols_by_date);
self
}
pub fn with_dividend_reinvestment(mut self, enabled: bool) -> Self { pub fn with_dividend_reinvestment(mut self, enabled: bool) -> Self {
self.dividend_reinvestment = enabled; self.dividend_reinvestment = enabled;
self self
@@ -2620,22 +2610,8 @@ where
let on_day_open_orders = self.open_order_views(); let on_day_open_orders = self.open_order_views();
let decision_quote_times = self.strategy.decision_quote_times(); let decision_quote_times = self.strategy.decision_quote_times();
if self.execution_quote_loader.is_some() && !decision_quote_times.is_empty() { if self.execution_quote_loader.is_some() && !decision_quote_times.is_empty() {
if let Some(preplanned) = self let decision_quote_symbols =
.preplanned_decision_quote_symbols_by_date self.strategy.decision_quote_symbols(&StrategyContext {
.as_ref()
.map(Arc::clone)
{
let empty_symbols = BTreeSet::new();
let decision_quote_symbols =
preplanned.get(&execution_date).unwrap_or(&empty_symbols);
self.ensure_execution_quotes_for_symbols_at_times(
execution_date,
decision_quote_symbols,
&decision_quote_times,
)?;
} else {
let decision_quote_symbols =
self.strategy.decision_quote_symbols(&StrategyContext {
execution_date, execution_date,
decision_date, decision_date,
decision_index, decision_index,
@@ -2653,13 +2629,12 @@ where
), ),
order_events: result.order_events.as_slice(), order_events: result.order_events.as_slice(),
fills: result.fills.as_slice(), fills: result.fills.as_slice(),
})?; })?;
self.ensure_execution_quotes_for_symbols_at_times( self.ensure_execution_quotes_for_symbols_at_times(
execution_date, execution_date,
&decision_quote_symbols, &decision_quote_symbols,
&decision_quote_times, &decision_quote_times,
)?; )?;
}
} }
self.ensure_execution_quotes_for_portfolio_times( self.ensure_execution_quotes_for_portfolio_times(
execution_date, execution_date,
@@ -5,7 +5,7 @@ use fidc_core::{
Instrument, IntradayExecutionQuote, MatchingType, OrderIntent, PriceField, Strategy, StrategyContext, Instrument, IntradayExecutionQuote, MatchingType, OrderIntent, PriceField, Strategy, StrategyContext,
StrategyDecision, StrategyDecision,
}; };
use std::collections::{BTreeMap, BTreeSet}; use std::collections::BTreeSet;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
fn d(year: i32, month: u32, day: u32) -> NaiveDate { fn d(year: i32, month: u32, day: u32) -> NaiveDate {
@@ -163,7 +163,7 @@ fn single_day_quote_plan_data(date: NaiveDate) -> DataSet {
} }
#[test] #[test]
fn runtime_account_dependent_quote_scope_cannot_be_replaced_by_an_empty_preplan() { fn runtime_account_dependent_quote_scope_uses_the_actual_account() {
struct AccountDependentQuoteReader; struct AccountDependentQuoteReader;
impl Strategy for AccountDependentQuoteReader { impl Strategy for AccountDependentQuoteReader {
fn name(&self) -> &str { "account_dependent_quote_reader" } fn name(&self) -> &str { "account_dependent_quote_reader" }
@@ -196,13 +196,12 @@ fn runtime_account_dependent_quote_scope_cannot_be_replaced_by_an_empty_preplan(
timestamp:request.date.and_time(t(10,17,59)), last_price:10.0,bid1:10.0,ask1:10.0, timestamp:request.date.and_time(t(10,17,59)), last_price:10.0,bid1:10.0,ask1:10.0,
bid1_volume:10_000,ask1_volume:10_000,volume_delta:10_000,amount_delta:100_000.0, bid1_volume:10_000,ask1_volume:10_000,volume_delta:10_000,amount_delta:100_000.0,
trading_phase:Some("continuous".into()), trading_phase:Some("continuous".into()),
}).collect())) }).collect()));
.with_preplanned_decision_quote_symbols_by_date(Arc::new(BTreeMap::new()));
engine.run().expect("account-dependent quote planning"); engine.run().expect("account-dependent quote planning");
} }
#[test] #[test]
fn engine_uses_preplanned_decision_symbols_without_recomputing_strategy_plan() { fn engine_resolves_the_runtime_strategy_scope_when_a_loader_exists() {
let date = d(2026, 1, 5); let date = d(2026, 1, 5);
let data = single_day_quote_plan_data(date); let data = single_day_quote_plan_data(date);
let broker = BrokerSimulator::new_with_execution_price( let broker = BrokerSimulator::new_with_execution_price(
@@ -225,10 +224,6 @@ fn engine_uses_preplanned_decision_symbols_without_recomputing_strategy_plan() {
symbol_plan_calls: Arc::clone(&symbol_plan_calls), symbol_plan_calls: Arc::clone(&symbol_plan_calls),
}; };
let captured_loader_calls = Arc::clone(&loader_calls); let captured_loader_calls = Arc::clone(&loader_calls);
let preplanned = Arc::new(BTreeMap::from([(
date,
BTreeSet::from(["000001.SZ".to_string()]),
)]));
let mut engine = BacktestEngine::new(data, strategy, broker, config) let mut engine = BacktestEngine::new(data, strategy, broker, config)
.with_execution_quote_loader(move |request| { .with_execution_quote_loader(move |request| {
*captured_loader_calls.lock().expect("loader counter mutex") += 1; *captured_loader_calls.lock().expect("loader counter mutex") += 1;
@@ -249,20 +244,19 @@ fn engine_uses_preplanned_decision_symbols_without_recomputing_strategy_plan() {
trading_phase: Some("continuous".to_string()), trading_phase: Some("continuous".to_string()),
}) })
.collect()) .collect())
}) });
.with_preplanned_decision_quote_symbols_by_date(preplanned);
engine.run().expect("backtest should run"); engine.run().expect("backtest should run");
assert_eq!( assert_eq!(
*symbol_plan_calls.lock().expect("symbol plan counter mutex"), *symbol_plan_calls.lock().expect("symbol plan counter mutex"),
0, 1,
"the strategy plan must not be recomputed after a complete plan is supplied" "quote planning must use the actual run context"
); );
assert_eq!( assert_eq!(
*loader_calls.lock().expect("loader counter mutex"), *loader_calls.lock().expect("loader counter mutex"),
1, 0,
"the supplied symbols must still pass through the normal quote loader" "an empty runtime scope must not fetch unrequested symbols"
); );
} }