test: declare historical audit assumptions in non-liquidity fixtures

This commit is contained in:
boris
2026-09-12 06:39:34 +08:00
parent 4d3a9e0e5b
commit 053f880e34
7 changed files with 124 additions and 78 deletions
+58 -38
View File
@@ -1484,8 +1484,6 @@ where
) -> Result<BrokerExecutionReport, BacktestError> {
if self.volume_limit {
self.volume_rate.map_err(|error| BacktestError::Execution(error.to_string()))?;
self.volume_capacity_mode.validate(true, self.matching_type_uses_intraday_quotes())
.map_err(|error| BacktestError::Execution(error.to_string()))?;
}
let mut session = std::mem::take(&mut *self.execution_session.borrow_mut());
session.activate(date);
@@ -4252,12 +4250,8 @@ where
.saturating_sub(self.reserved_open_sell_quantity(symbol, None));
match self.market_fillable_quantity(
snapshot,
OrderSide::Sell,
sellable.min(current_qty),
minimum_order_quantity,
order_step_size,
0,
sellable >= current_qty,
false,
) {
Ok(quantity) => {
let quantity = quantity.min(sellable).min(current_qty);
@@ -4300,11 +4294,7 @@ where
}
match self.market_fillable_quantity(
snapshot,
OrderSide::Buy,
u32::MAX,
minimum_order_quantity,
order_step_size,
0,
false,
) {
Ok(quantity) => {
@@ -4573,14 +4563,12 @@ where
} else {
None
};
self.volume_capacity_mode.validate(self.volume_limit, algo_request.is_some() || self.matching_type_uses_intraday_quotes())
.map_err(|error| BacktestError::Execution(error.to_string()))?;
let market_limited_qty = self.market_fillable_quantity(
snapshot,
OrderSide::Sell,
requested_qty.min(sellable),
self.minimum_order_quantity(data, symbol),
self.order_step_size(data, symbol),
*intraday_turnover.get(symbol).unwrap_or(&0),
requested_qty >= position.quantity && sellable >= position.quantity,
algo_request.is_some(),
);
let fillable_qty = match market_limited_qty {
Ok(quantity) => {
@@ -6382,14 +6370,12 @@ where
}
let mut partial_fill_reason = None;
self.volume_capacity_mode.validate(self.volume_limit, algo_request.is_some() || self.matching_type_uses_intraday_quotes())
.map_err(|error| BacktestError::Execution(error.to_string()))?;
let market_limited_qty = self.market_fillable_quantity(
snapshot,
OrderSide::Buy,
requested_qty,
self.minimum_order_quantity(data, symbol),
self.order_step_size(data, symbol),
*intraday_turnover.get(symbol).unwrap_or(&0),
false,
algo_request.is_some(),
);
let constrained_qty = match market_limited_qty {
Ok(quantity) => {
@@ -7286,20 +7272,15 @@ where
fn market_fillable_quantity(
&self,
snapshot: &crate::data::DailyMarketSnapshot,
side: OrderSide,
requested_qty: u32,
minimum_order_quantity: u32,
order_step_size: u32,
consumed_turnover: u32,
allow_odd_lot_sell: bool,
algorithmic_order: bool,
) -> Result<u32, String> {
if requested_qty == 0 {
return Ok(0);
}
let _ = (side, minimum_order_quantity, order_step_size, consumed_turnover, allow_odd_lot_sell);
if self.inactive_limit && snapshot.paused { return Err("paused".into()); }
self.volume_capacity_mode.validate(self.volume_limit, self.matching_type_uses_intraday_quotes())
self.volume_capacity_mode.validate(self.volume_limit, algorithmic_order || self.matching_type_uses_intraday_quotes())
.map_err(|error| error.to_string())?;
// Per-observation limits are applied to each actual quote below. The
// session-audit model must never size this order from the day's total.
@@ -9412,7 +9393,42 @@ mod tests {
}
#[test]
fn current_bar_close_volume_limit_uses_daily_volume_when_minute_volume_missing() {
fn daily_session_volume_changes_only_audit_not_opening_fills() {
use crate::execution_capacity::VolumeCapacityMode;
let run = |volume: u64, mode: VolumeCapacityMode| {
let mut market = limit_test_snapshot();
market.volume = volume;
let date = market.date;
let data = DataSet::from_components_with_actions_and_quotes(
vec![limit_test_instrument()], vec![market], vec![],
vec![limit_test_candidate(true, true)], vec![limit_test_benchmark()], vec![], vec![],
).unwrap();
let mut portfolio = PortfolioState::new(100_000.0);
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
.with_volume_capacity_mode(mode).with_liquidity_limit(false);
let decision = StrategyDecision { order_intents: vec![OrderIntent::Shares {
symbol: "000001.SZ".into(), quantity: 1_000, reason: "capacity_test".into(),
}], ..StrategyDecision::default() };
let before = portfolio.cash();
let outcome = broker.execute(date, &mut portfolio, &data, &decision);
if outcome.is_err() { assert_eq!(portfolio.cash(), before); }
let audit = broker.audit_completed_session_capacity(date, &data).unwrap();
(outcome, portfolio.cash(), audit)
};
let (strict, _, _) = run(1_000_000, VolumeCapacityMode::ExecutionObservation);
assert!(strict.unwrap_err().to_string().contains("execution-time capacity is missing"));
let (a, cash_a, audit_a) = run(100, VolumeCapacityMode::SessionCapacityAudit);
let (b, cash_b, audit_b) = run(1_000_000, VolumeCapacityMode::SessionCapacityAudit);
let a = a.unwrap(); let b = b.unwrap();
assert_eq!(a.fill_events.len(), 1);
assert_eq!(serde_json::to_value(&a.fill_events).unwrap(), serde_json::to_value(&b.fill_events).unwrap());
assert_eq!(cash_a, cash_b);
assert_eq!(audit_a[0].filled_shares, 1_000);
assert!(!audit_a[0].passed); assert!(audit_b[0].passed);
}
#[test]
fn daily_capacity_requires_a_timed_observation_instead_of_falling_back_to_total_volume() {
let mut snapshot = limit_test_snapshot();
snapshot.minute_volume = 0;
snapshot.volume = 1_000_000;
@@ -9428,13 +9444,13 @@ mod tests {
.with_liquidity_limit(true);
let fillable =
broker.market_fillable_quantity(&snapshot, OrderSide::Buy, 5_000, 100, 100, 0, false);
broker.market_fillable_quantity(&snapshot, 5_000, false);
assert_eq!(fillable, Ok(5_000));
assert!(fillable.unwrap_err().contains("daily session volume cannot size an earlier fill"));
}
#[test]
fn volume_limit_uses_floor_for_odd_lot_sell() {
fn session_capacity_audit_never_caps_an_early_odd_lot_sell() {
let mut snapshot = limit_test_snapshot();
snapshot.minute_volume = 0;
snapshot.volume = 3;
@@ -9444,18 +9460,19 @@ mod tests {
PriceField::Close,
)
.with_matching_type(MatchingType::CurrentBarClose)
.with_volume_capacity_mode(crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
.with_volume_limit(true)
.with_volume_percent(0.5)
.with_liquidity_limit(false);
let fillable =
broker.market_fillable_quantity(&snapshot, OrderSide::Sell, 10, 100, 100, 0, true);
broker.market_fillable_quantity(&snapshot, 10, false);
assert_eq!(fillable, Ok(1));
assert_eq!(fillable, Ok(10));
}
#[test]
fn current_bar_close_volume_limit_rejects_daily_zero_volume() {
fn session_audit_does_not_infer_an_opening_suspension_from_future_zero_volume() {
let mut snapshot = limit_test_snapshot();
snapshot.minute_volume = 0;
snapshot.volume = 0;
@@ -9465,13 +9482,16 @@ mod tests {
PriceField::Close,
)
.with_matching_type(MatchingType::CurrentBarClose)
.with_volume_capacity_mode(crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
.with_volume_limit(true)
.with_liquidity_limit(false);
let fillable =
broker.market_fillable_quantity(&snapshot, OrderSide::Buy, 5_000, 100, 100, 0, false);
broker.market_fillable_quantity(&snapshot, 5_000, false);
assert_eq!(fillable, Err("daily no volume".to_string()));
assert_eq!(fillable, Ok(5_000));
snapshot.paused = true;
assert_eq!(broker.market_fillable_quantity(&snapshot, 5_000, false), Err("paused".into()));
}
#[test]
@@ -9492,7 +9512,7 @@ mod tests {
.with_liquidity_limit(false);
let fillable =
broker.market_fillable_quantity(&snapshot, OrderSide::Buy, 5_000, 100, 100, 0, false);
broker.market_fillable_quantity(&snapshot, 5_000, false);
assert_eq!(fillable, Ok(5_000));
}