校验分钟订阅行情覆盖
This commit is contained in:
@@ -607,7 +607,19 @@ where
|
|||||||
.as_mut()
|
.as_mut()
|
||||||
.expect("checked execution quote loader")
|
.expect("checked execution quote loader")
|
||||||
.as_mut()(request)?;
|
.as_mut()(request)?;
|
||||||
|
let requested_symbol_set = requested_symbols.iter().cloned().collect::<BTreeSet<_>>();
|
||||||
|
if let Some(quote) = quotes.iter().find(|quote| {
|
||||||
|
quote.date != execution_date || !requested_symbol_set.contains("e.symbol)
|
||||||
|
}) {
|
||||||
|
return Err(BacktestError::Execution(format!(
|
||||||
|
"execution quote loader returned a row outside the request: requested_date={} actual_date={} symbol={}",
|
||||||
|
execution_date, quote.date, quote.symbol
|
||||||
|
)));
|
||||||
|
}
|
||||||
self.data.add_execution_quotes(quotes);
|
self.data.add_execution_quotes(quotes);
|
||||||
|
if start_time.is_none() && end_time.is_none() {
|
||||||
|
self.validate_full_day_execution_quote_coverage(execution_date, &requested_symbols)?;
|
||||||
|
}
|
||||||
for symbol in requested_symbols {
|
for symbol in requested_symbols {
|
||||||
self.execution_quote_request_cache.insert((
|
self.execution_quote_request_cache.insert((
|
||||||
execution_date,
|
execution_date,
|
||||||
@@ -619,6 +631,48 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_full_day_execution_quote_coverage(
|
||||||
|
&self,
|
||||||
|
execution_date: NaiveDate,
|
||||||
|
requested_symbols: &[String],
|
||||||
|
) -> Result<(), BacktestError> {
|
||||||
|
let mut missing_active = Vec::new();
|
||||||
|
let mut paused_with_quotes = Vec::new();
|
||||||
|
let mut missing_daily_market = Vec::new();
|
||||||
|
for symbol in requested_symbols {
|
||||||
|
let Some(_candidate) = self.data.candidate(execution_date, symbol) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(market) = self.data.market(execution_date, symbol) else {
|
||||||
|
missing_daily_market.push(symbol.clone());
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let has_quotes = !self
|
||||||
|
.data
|
||||||
|
.execution_quotes_on(execution_date, symbol)
|
||||||
|
.is_empty();
|
||||||
|
if market.paused {
|
||||||
|
if has_quotes {
|
||||||
|
paused_with_quotes.push(symbol.clone());
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if market.volume > 0 && !has_quotes {
|
||||||
|
missing_active.push(symbol.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if missing_daily_market.is_empty()
|
||||||
|
&& missing_active.is_empty()
|
||||||
|
&& paused_with_quotes.is_empty()
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(BacktestError::Execution(format!(
|
||||||
|
"full-minute subscription coverage mismatch on {}: missing_daily_market={:?}, missing_active_minute_bars={:?}, paused_with_minute_bars={:?}",
|
||||||
|
execution_date, missing_daily_market, missing_active, paused_with_quotes
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
fn ensure_execution_quotes_for_portfolio_times(
|
fn ensure_execution_quotes_for_portfolio_times(
|
||||||
&mut self,
|
&mut self,
|
||||||
execution_date: NaiveDate,
|
execution_date: NaiveDate,
|
||||||
@@ -4815,6 +4869,70 @@ mod tests {
|
|||||||
.expect("backtest run")
|
.expect("backtest run")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn full_day_coverage_engine(
|
||||||
|
data: DataSet,
|
||||||
|
date: NaiveDate,
|
||||||
|
) -> BacktestEngine<BuyWhenDecisionDateStrategy, ChinaAShareCostModel, ChinaEquityRuleHooks>
|
||||||
|
{
|
||||||
|
let broker = BrokerSimulator::new_with_execution_price(
|
||||||
|
ChinaAShareCostModel::default(),
|
||||||
|
ChinaEquityRuleHooks,
|
||||||
|
PriceField::Last,
|
||||||
|
)
|
||||||
|
.with_matching_type(MatchingType::MinuteLast)
|
||||||
|
.with_volume_limit(false)
|
||||||
|
.with_liquidity_limit(false);
|
||||||
|
BacktestEngine::new(
|
||||||
|
data,
|
||||||
|
BuyWhenDecisionDateStrategy {
|
||||||
|
decision_date: date,
|
||||||
|
},
|
||||||
|
broker,
|
||||||
|
BacktestConfig {
|
||||||
|
initial_cash: 100_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::Last,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_minute_coverage_rejects_missing_active_bars_but_allows_paused_or_zero_volume() {
|
||||||
|
let first = d(2025, 1, 2);
|
||||||
|
let second = d(2025, 1, 3);
|
||||||
|
let active = full_day_coverage_engine(dataset(), first);
|
||||||
|
let error = active
|
||||||
|
.validate_full_day_execution_quote_coverage(first, &[SYMBOL.to_string()])
|
||||||
|
.expect_err("active stock with daily volume requires minute bars");
|
||||||
|
assert!(
|
||||||
|
error.to_string().contains("missing_active_minute_bars"),
|
||||||
|
"{error}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let paused_data = dataset_with(
|
||||||
|
market_with_state(first, 10.0, 10.0, true, 11.0, 9.0),
|
||||||
|
market(second, 10.0, 10.0),
|
||||||
|
candidate_with_state(first, true, false),
|
||||||
|
candidate(second),
|
||||||
|
);
|
||||||
|
full_day_coverage_engine(paused_data, first)
|
||||||
|
.validate_full_day_execution_quote_coverage(first, &[SYMBOL.to_string()])
|
||||||
|
.expect("paused stock may have no minute bars");
|
||||||
|
|
||||||
|
let zero_volume_data = dataset_with(
|
||||||
|
market_with_volume(first, 10.0, 10.0, 0),
|
||||||
|
market(second, 10.0, 10.0),
|
||||||
|
candidate(first),
|
||||||
|
candidate(second),
|
||||||
|
);
|
||||||
|
full_day_coverage_engine(zero_volume_data, first)
|
||||||
|
.validate_full_day_execution_quote_coverage(first, &[SYMBOL.to_string()])
|
||||||
|
.expect("zero-volume stock may have no minute bars");
|
||||||
|
}
|
||||||
|
|
||||||
fn run_scheduled_next_open_with_dataset(dataset: DataSet) -> super::BacktestResult {
|
fn run_scheduled_next_open_with_dataset(dataset: DataSet) -> super::BacktestResult {
|
||||||
run_scheduled_next_open_with_dataset_and_broker(
|
run_scheduled_next_open_with_dataset_and_broker(
|
||||||
dataset,
|
dataset,
|
||||||
|
|||||||
Reference in New Issue
Block a user