修复AiQuant策略表达式回测执行语义

This commit is contained in:
boris
2026-06-15 11:16:04 +08:00
parent d3d08276ae
commit 1c31fa80d2
8 changed files with 1227 additions and 292 deletions
+113 -3
View File
@@ -1,6 +1,6 @@
use std::collections::{BTreeMap, BTreeSet};
use chrono::{Datelike, NaiveDate};
use chrono::{Datelike, NaiveDate, NaiveTime};
use serde::Serialize;
use thiserror::Error;
@@ -523,8 +523,35 @@ where
return Ok(());
}
let start_time = start_time.or_else(|| self.broker.intraday_execution_start_time());
let caller_start_time = start_time;
let caller_end_time = end_time;
let start_time = caller_start_time.or_else(|| self.broker.intraday_execution_start_time());
let mut symbols = execution_quote_symbols_for_decision(decision, portfolio, open_orders);
self.load_missing_execution_quotes(execution_date, start_time, end_time, &mut symbols)?;
if caller_start_time.is_none() && caller_end_time.is_none() {
for ((intent_start_time, intent_end_time), mut intent_symbols) in
algo_execution_quote_windows_for_decision(decision, portfolio)
{
self.load_missing_execution_quotes(
execution_date,
intent_start_time,
intent_end_time,
&mut intent_symbols,
)?;
}
}
Ok(())
}
fn load_missing_execution_quotes(
&mut self,
execution_date: NaiveDate,
start_time: Option<NaiveTime>,
end_time: Option<NaiveTime>,
symbols: &mut BTreeSet<String>,
) -> Result<(), BacktestError> {
symbols.retain(|symbol| {
!has_execution_quote_in_window(&self.data, execution_date, symbol, start_time, end_time)
});
@@ -536,7 +563,7 @@ where
date: execution_date,
start_time,
end_time,
symbols,
symbols: std::mem::take(symbols),
};
let quotes = self
.execution_quote_loader
@@ -547,6 +574,35 @@ where
Ok(())
}
fn ensure_execution_quotes_for_portfolio_times(
&mut self,
execution_date: NaiveDate,
portfolio: &PortfolioState,
quote_times: &[NaiveTime],
) -> Result<(), BacktestError> {
if self.execution_quote_loader.is_none() || quote_times.is_empty() {
return Ok(());
}
let base_symbols = portfolio
.positions()
.keys()
.cloned()
.collect::<BTreeSet<_>>();
if base_symbols.is_empty() {
return Ok(());
}
for quote_time in quote_times {
let mut symbols = base_symbols.clone();
self.load_missing_execution_quotes(
execution_date,
Some(*quote_time),
Some(*quote_time),
&mut symbols,
)?;
}
Ok(())
}
fn apply_strategy_directives(
&mut self,
execution_date: NaiveDate,
@@ -1875,6 +1931,12 @@ where
"on_day:pre",
)?;
let on_day_open_orders = self.open_order_views();
let decision_quote_times = self.strategy.decision_quote_times();
self.ensure_execution_quotes_for_portfolio_times(
execution_date,
&portfolio,
&decision_quote_times,
)?;
let mut decision = decision_slot
.map(|(decision_idx, decision_date)| {
self.strategy.on_day(&StrategyContext {
@@ -3283,6 +3345,54 @@ fn execution_quote_symbols_for_decision(
symbols
}
fn algo_execution_quote_windows_for_decision(
decision: &StrategyDecision,
portfolio: &PortfolioState,
) -> BTreeMap<(Option<NaiveTime>, Option<NaiveTime>), BTreeSet<String>> {
let mut groups = BTreeMap::<(Option<NaiveTime>, Option<NaiveTime>), BTreeSet<String>>::new();
for intent in &decision.order_intents {
match intent {
OrderIntent::AlgoValue {
symbol,
start_time,
end_time,
..
}
| OrderIntent::AlgoPercent {
symbol,
start_time,
end_time,
..
} => {
if start_time.is_some() || end_time.is_some() {
groups
.entry((*start_time, *end_time))
.or_default()
.insert(symbol.clone());
}
}
OrderIntent::TargetPortfolioSmart {
target_weights,
order_prices:
Some(TargetPortfolioOrderPricing::AlgoOrder {
start_time,
end_time,
..
}),
..
} => {
if start_time.is_some() || end_time.is_some() {
let symbols = groups.entry((*start_time, *end_time)).or_default();
symbols.extend(portfolio.positions().keys().cloned());
symbols.extend(target_weights.keys().cloned());
}
}
_ => {}
}
}
groups
}
fn collect_scheduled_decisions<S: Strategy>(
strategy: &mut S,
scheduler: &Scheduler<'_>,