|
|
|
@@ -1298,6 +1298,7 @@ where
|
|
|
|
|
submission_time,
|
|
|
|
|
);
|
|
|
|
|
if self.broker.execution_price_field() != PriceField::Last
|
|
|
|
|
&& !self.broker.matching_type_uses_intraday_quotes()
|
|
|
|
|
&& !decision_has_algo_execution(decision)
|
|
|
|
|
&& post_close_window.is_none()
|
|
|
|
|
{
|
|
|
|
@@ -8941,6 +8942,47 @@ mod tests {
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn next_open_observation_loads_quotes_even_when_execution_price_is_open() {
|
|
|
|
|
use crate::execution_capacity::VolumeCapacityMode;
|
|
|
|
|
let date = d(2025, 1, 3);
|
|
|
|
|
let signal = d(2025, 1, 2);
|
|
|
|
|
let open = NaiveTime::from_hms_opt(9, 30, 0).unwrap();
|
|
|
|
|
for mode in [VolumeCapacityMode::ExecutionObservation, VolumeCapacityMode::SessionCapacityAudit] {
|
|
|
|
|
let mut broker = BrokerSimulator::new_with_execution_price(
|
|
|
|
|
ChinaAShareCostModel::default(), ChinaEquityRuleHooks, PriceField::Open,
|
|
|
|
|
).with_matching_type(MatchingType::NextBarOpen)
|
|
|
|
|
.with_volume_limit(true).with_volume_capacity_mode(mode).with_liquidity_limit(false);
|
|
|
|
|
if mode == VolumeCapacityMode::ExecutionObservation {
|
|
|
|
|
broker = broker.with_intraday_execution_start_time(open);
|
|
|
|
|
}
|
|
|
|
|
let calls = Arc::new(Mutex::new(Vec::new()));
|
|
|
|
|
let captured = calls.clone();
|
|
|
|
|
let mut engine = BacktestEngine::new(dataset(), BuyWhenDecisionDateStrategy { decision_date: signal }, broker,
|
|
|
|
|
BacktestConfig { initial_cash: 100_000., benchmark_code: "000852.SH".into(),
|
|
|
|
|
start_date: Some(signal), end_date: Some(date), decision_lag_trading_days: 1,
|
|
|
|
|
execution_price_field: PriceField::Open })
|
|
|
|
|
.with_execution_quote_loader(move |request| {
|
|
|
|
|
captured.lock().unwrap().push(request.clone());
|
|
|
|
|
Ok(clock_probe_data(request.date, &[(9,30,10.)]).snapshot_components().execution_quotes)
|
|
|
|
|
});
|
|
|
|
|
let decision = StrategyDecision { order_intents: vec![OrderIntent::Shares {
|
|
|
|
|
symbol: SYMBOL.into(), quantity: 100, reason: "next-open-loader-regression".into(),
|
|
|
|
|
}], ..Default::default() };
|
|
|
|
|
engine.ensure_execution_quotes_for_decision(date, signal, &PortfolioState::new(100_000.), &[], &decision, None, None).unwrap();
|
|
|
|
|
let calls = calls.lock().unwrap();
|
|
|
|
|
if mode == VolumeCapacityMode::ExecutionObservation {
|
|
|
|
|
assert_eq!(calls.len(), 1);
|
|
|
|
|
assert_eq!(calls[0].date, date);
|
|
|
|
|
assert_eq!(calls[0].start_time, Some(open));
|
|
|
|
|
assert_eq!(calls[0].symbols, BTreeSet::from([SYMBOL.to_string()]));
|
|
|
|
|
assert_eq!(engine.data.execution_quotes_on(date, SYMBOL).len(), 1);
|
|
|
|
|
} else {
|
|
|
|
|
assert!(calls.is_empty(), "daily audit must not silently become an opening-liquidity model");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn full_minute_coverage_rejects_missing_active_bars_but_allows_paused_or_zero_volume() {
|
|
|
|
|
let first = d(2025, 1, 2);
|
|
|
|
|