fix(engine): resolve decision quote scope from the actual run context
This commit is contained in:
@@ -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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user