修正执行价quote多时间加载

This commit is contained in:
boris
2026-06-16 00:05:34 +08:00
parent c2de9d8e83
commit ff145300b4
3 changed files with 259 additions and 7 deletions
+5 -3
View File
@@ -362,8 +362,7 @@ where
symbol: &str,
snapshot: &crate::data::DailyMarketSnapshot,
) -> f64 {
if self.aiquant_rqalpha_execution_rules && self.execution_price_field == PriceField::Last
{
if self.aiquant_rqalpha_execution_rules && self.execution_price_field == PriceField::Last {
let start_cursor = self
.runtime_intraday_start_time
.get()
@@ -5395,7 +5394,10 @@ mod tests {
)
.expect("process target value");
assert_eq!(portfolio.position(symbol).map(|pos| pos.quantity), Some(21_400));
assert_eq!(
portfolio.position(symbol).map(|pos| pos.quantity),
Some(21_400)
);
let order = report.order_events.last().expect("target value order");
assert_eq!(order.requested_quantity, 200);
assert_eq!(order.filled_quantity, 200);
+19
View File
@@ -341,6 +341,8 @@ pub struct BacktestEngine<S, C, R> {
futures_cost_model: FuturesTransactionCostModel,
futures_validation_config: FuturesValidationConfig,
execution_quote_loader: Option<ExecutionQuoteLoader>,
execution_quote_request_cache:
BTreeSet<(NaiveDate, String, Option<NaiveTime>, Option<NaiveTime>)>,
}
impl<S, C, R> BacktestEngine<S, C, R> {
@@ -369,6 +371,7 @@ impl<S, C, R> BacktestEngine<S, C, R> {
futures_cost_model: FuturesTransactionCostModel::default(),
futures_validation_config: FuturesValidationConfig::default(),
execution_quote_loader: None,
execution_quote_request_cache: BTreeSet::new(),
}
}
@@ -553,12 +556,20 @@ where
symbols: &mut BTreeSet<String>,
) -> Result<(), BacktestError> {
symbols.retain(|symbol| {
let request_key = (execution_date, symbol.clone(), start_time, end_time);
if self.execution_quote_request_cache.contains(&request_key) {
return false;
}
if start_time.is_some() && end_time.is_none() {
return true;
}
!has_execution_quote_in_window(&self.data, execution_date, symbol, start_time, end_time)
});
if symbols.is_empty() {
return Ok(());
}
let requested_symbols = symbols.iter().cloned().collect::<Vec<_>>();
let request = ExecutionQuoteRequest {
date: execution_date,
start_time,
@@ -571,6 +582,14 @@ where
.expect("checked execution quote loader")
.as_mut()(request)?;
self.data.add_execution_quotes(quotes);
for symbol in requested_symbols {
self.execution_quote_request_cache.insert((
execution_date,
symbol,
start_time,
end_time,
));
}
Ok(())
}
@@ -1,10 +1,11 @@
use chrono::{NaiveDate, NaiveTime};
use chrono::{Duration, NaiveDate, NaiveTime};
use fidc_core::{
BacktestConfig, BacktestEngine, BenchmarkSnapshot, BrokerSimulator, CandidateEligibility,
ChinaAShareCostModel, ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet,
IntradayExecutionQuote, MatchingType, OrderIntent, PriceField, Strategy, StrategyContext,
StrategyDecision,
};
use std::sync::{Arc, Mutex};
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("valid date")
@@ -209,9 +210,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
.map(|symbol| IntradayExecutionQuote {
date: request.date,
symbol,
timestamp: request
.date
.and_time(t(10, 39, 59)),
timestamp: request.date.and_time(t(10, 39, 59)),
last_price: if request.date == second { 11.0 } else { 10.0 },
bid1: if request.date == second { 11.0 } else { 10.0 },
ask1: if request.date == second { 11.0 } else { 10.0 },
@@ -226,3 +225,235 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
engine.run().expect("backtest should run");
}
#[derive(Default)]
struct MultiTimeDecisionQuoteReader {
day_count: usize,
}
impl Strategy for MultiTimeDecisionQuoteReader {
fn name(&self) -> &str {
"multi_time_decision_quote_reader"
}
fn decision_quote_times(&self) -> Vec<NaiveTime> {
vec![t(10, 31, 0), t(10, 40, 0)]
}
fn on_day(
&mut self,
ctx: &StrategyContext<'_>,
) -> Result<StrategyDecision, fidc_core::BacktestError> {
self.day_count += 1;
if self.day_count == 1 {
return Ok(StrategyDecision {
order_intents: vec![OrderIntent::Value {
symbol: "000001.SZ".to_string(),
value: 5_000.0,
reason: "seed_position".to_string(),
}],
..StrategyDecision::default()
});
}
let quote_times = ctx
.data
.execution_quotes_on(ctx.execution_date, "000001.SZ")
.iter()
.map(|quote| quote.timestamp.time())
.collect::<Vec<_>>();
assert!(
quote_times.contains(&t(10, 30, 59)),
"10:31 decision quote must be loaded"
);
assert!(
quote_times.contains(&t(10, 39, 59)),
"10:40 decision quote must not be skipped because 10:31 was loaded"
);
Ok(StrategyDecision::default())
}
}
#[test]
fn engine_loads_distinct_decision_quote_times_on_same_day() {
let first = d(2026, 1, 5);
let second = d(2026, 1, 6);
let data = DataSet::from_components(
Vec::new(),
vec![
DailyMarketSnapshot {
date: first,
symbol: "000001.SZ".to_string(),
timestamp: Some("2026-01-05 15:00:00".to_string()),
day_open: 10.0,
open: 10.0,
high: 10.2,
low: 9.9,
close: 10.0,
last_price: 10.0,
bid1: 10.0,
ask1: 10.0,
prev_close: 9.8,
volume: 10_000,
tick_volume: 1_000,
bid1_volume: 10_000,
ask1_volume: 10_000,
trading_phase: Some("continuous".to_string()),
paused: false,
upper_limit: 10.78,
lower_limit: 8.82,
price_tick: 0.01,
},
DailyMarketSnapshot {
date: second,
symbol: "000001.SZ".to_string(),
timestamp: Some("2026-01-06 15:00:00".to_string()),
day_open: 10.5,
open: 10.5,
high: 11.2,
low: 10.4,
close: 10.6,
last_price: 10.6,
bid1: 10.6,
ask1: 10.6,
prev_close: 10.0,
volume: 10_000,
tick_volume: 1_000,
bid1_volume: 10_000,
ask1_volume: 10_000,
trading_phase: Some("continuous".to_string()),
paused: false,
upper_limit: 11.0,
lower_limit: 9.0,
price_tick: 0.01,
},
],
vec![
DailyFactorSnapshot {
date: first,
symbol: "000001.SZ".to_string(),
market_cap_bn: 10.0,
free_float_cap_bn: 10.0,
pe_ttm: 10.0,
turnover_ratio: None,
effective_turnover_ratio: None,
extra_factors: Default::default(),
},
DailyFactorSnapshot {
date: second,
symbol: "000001.SZ".to_string(),
market_cap_bn: 10.0,
free_float_cap_bn: 10.0,
pe_ttm: 10.0,
turnover_ratio: None,
effective_turnover_ratio: None,
extra_factors: Default::default(),
},
],
vec![
CandidateEligibility {
date: first,
symbol: "000001.SZ".to_string(),
is_st: false,
is_new_listing: false,
is_paused: false,
allow_buy: true,
allow_sell: true,
is_kcb: false,
is_one_yuan: false,
},
CandidateEligibility {
date: second,
symbol: "000001.SZ".to_string(),
is_st: false,
is_new_listing: false,
is_paused: false,
allow_buy: true,
allow_sell: true,
is_kcb: false,
is_one_yuan: false,
},
],
vec![
BenchmarkSnapshot {
date: first,
benchmark: "000852.SH".to_string(),
open: 1000.0,
close: 1000.0,
prev_close: 990.0,
volume: 1_000_000,
},
BenchmarkSnapshot {
date: second,
benchmark: "000852.SH".to_string(),
open: 1000.0,
close: 1001.0,
prev_close: 1000.0,
volume: 1_000_000,
},
],
)
.expect("dataset");
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Last,
)
.with_matching_type(MatchingType::NextTickLast)
.with_intraday_execution_start_time(t(10, 40, 0));
let config = BacktestConfig {
initial_cash: 10_000.0,
benchmark_code: "000852.SH".to_string(),
start_date: Some(first),
end_date: Some(second),
decision_lag_trading_days: 0,
execution_price_field: PriceField::Last,
};
let requests = Arc::new(Mutex::new(Vec::<(NaiveDate, NaiveTime)>::new()));
let captured_requests = Arc::clone(&requests);
let mut engine = BacktestEngine::new(
data,
MultiTimeDecisionQuoteReader::default(),
broker,
config,
)
.with_execution_quote_loader(move |request| {
let start_time = request
.start_time
.expect("decision quote loader request must include start_time");
captured_requests
.lock()
.expect("request mutex")
.push((request.date, start_time));
Ok(request
.symbols
.into_iter()
.map(|symbol| IntradayExecutionQuote {
date: request.date,
symbol,
timestamp: request.date.and_time(start_time) - Duration::seconds(1),
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,
trading_phase: Some("continuous".to_string()),
})
.collect())
});
engine.run().expect("backtest should run");
let requests = requests.lock().expect("request mutex").clone();
assert!(
requests.contains(&(second, t(10, 31, 0))),
"second-day 10:31 quote request is required"
);
assert!(
requests.contains(&(second, t(10, 40, 0))),
"second-day 10:40 quote request must not be skipped by earlier quote"
);
}