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));
}
@@ -14527,6 +14527,7 @@ mod tests {
active_datetime: None, order_events: &[], fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.signal_symbol = symbol.to_string();
cfg.max_positions = 1;
cfg.refresh_rate = 1;
@@ -14560,6 +14561,7 @@ mod tests {
active_datetime: None, order_events: &[], fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::generic();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.signal_symbol = symbol.into();
cfg.stock_filter_expr = "close > 0".into();
cfg.hold_until_exit_enabled = true;
@@ -15175,6 +15177,7 @@ mod tests {
}
let data = DataSet::from_components(parts.instruments, parts.market, parts.factors, parts.candidates, parts.benchmarks).unwrap();
let mut config = PlatformExprStrategyConfig::generic();
config.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
config.universe_include = Some(BTreeSet::from(["000001.SZ".to_owned()]));
config.signal_symbol = "000001.SZ".to_owned();
config.benchmark_symbol = "000852.SH".to_owned();
@@ -15419,6 +15422,7 @@ mod tests {
fills: &[],
};
let mut config = PlatformExprStrategyConfig::microcap_rotation();
config.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
config.signal_symbol = symbol.to_string();
config.refresh_rate = 1;
config.max_positions = 1;
@@ -15670,6 +15674,7 @@ mod tests {
fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.signal_symbol = "000001.SZ".to_string();
cfg.refresh_rate = 1;
cfg.max_positions = 3;
@@ -22375,6 +22380,7 @@ mod tests {
fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.signal_symbol = symbol.to_string();
cfg.exposure_expr = "1.0".to_string();
cfg.selection_limit_expr = "40".to_string();
@@ -22749,6 +22755,7 @@ mod tests {
fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.rotation_enabled = false;
cfg.daily_top_up_enabled = false;
cfg.signal_symbol = symbol.to_string();
@@ -23705,6 +23712,7 @@ mod tests {
fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.signal_symbol = symbol.to_string();
cfg.exposure_expr = "0.5".to_string();
cfg.selection_limit_expr = "40".to_string();
@@ -24961,6 +24969,7 @@ mod tests {
fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.intraday_execution_time = Some(NaiveTime::from_hms_opt(9, 33, 0).unwrap());
cfg.signal_symbol = signal.to_string();
cfg.max_positions = 1;
@@ -27428,6 +27437,7 @@ mod tests {
fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.signal_symbol = "000001.SZ".to_string();
cfg.refresh_rate = 99;
cfg.max_positions = 2;
@@ -27569,6 +27579,7 @@ mod tests {
fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.signal_symbol = "000001.SZ".to_string();
cfg.refresh_rate = 99;
cfg.max_positions = 2;
@@ -28359,6 +28370,7 @@ mod tests {
fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.signal_symbol = "000001.SZ".to_string();
cfg.refresh_rate = 99;
cfg.max_positions = 2;
@@ -28678,6 +28690,7 @@ mod tests {
.expect("dataset");
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.signal_symbol = "000001.SZ".to_string();
cfg.refresh_rate = 20;
cfg.max_positions = 2;
@@ -28726,6 +28739,7 @@ mod tests {
);
let mut dynamic_cfg = PlatformExprStrategyConfig::microcap_rotation();
dynamic_cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
dynamic_cfg.signal_symbol = "000001.SZ".to_string();
dynamic_cfg.refresh_rate = 20;
dynamic_cfg.refresh_rate_expr = "year >= 2024 ? 5 : 20".to_string();
@@ -28751,6 +28765,7 @@ mod tests {
);
let mut signal_dates_cfg = PlatformExprStrategyConfig::microcap_rotation();
signal_dates_cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
signal_dates_cfg.signal_symbol = "000001.SZ".to_string();
signal_dates_cfg.refresh_rate = 20;
signal_dates_cfg.max_positions = 2;
@@ -28786,6 +28801,7 @@ mod tests {
);
let mut no_retry_cfg = PlatformExprStrategyConfig::microcap_rotation();
no_retry_cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
no_retry_cfg.signal_symbol = "000001.SZ".to_string();
no_retry_cfg.refresh_rate = 15;
no_retry_cfg.max_positions = 2;
@@ -28953,6 +28969,7 @@ mod tests {
.expect("dataset");
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.signal_symbol = "000001.SZ".to_string();
cfg.refresh_rate = 10;
cfg.max_positions = 2;
@@ -29130,6 +29147,7 @@ mod tests {
fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.signal_symbol = "000001.SZ".to_string();
cfg.refresh_rate = 20;
cfg.max_positions = 2;
@@ -30343,6 +30361,7 @@ mod tests {
fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.rotation_enabled = false;
cfg.hold_until_exit_enabled = true;
cfg.signal_symbol = symbol.to_string();
@@ -31605,6 +31624,7 @@ mod tests {
fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.signal_symbol = "000001.SZ".to_string();
cfg.refresh_rate = 99;
cfg.max_positions = 2;
@@ -31758,6 +31778,7 @@ mod tests {
fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.signal_symbol = "000001.SZ".to_string();
cfg.refresh_rate = 99;
cfg.max_positions = 2;
@@ -31922,6 +31943,7 @@ mod tests {
fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.signal_symbol = "000001.SZ".to_string();
cfg.refresh_rate = 99;
cfg.max_positions = 2;
@@ -33943,6 +33965,7 @@ mod tests {
fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.signal_symbol = "000001.SZ".to_string();
cfg.refresh_rate = 99;
cfg.max_positions = 1;
@@ -34120,6 +34143,7 @@ mod tests {
fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.signal_symbol = "000001.SZ".to_string();
cfg.refresh_rate = 99;
cfg.max_positions = 1;
@@ -34162,6 +34186,7 @@ mod tests {
);
let mut filtered_cfg = PlatformExprStrategyConfig::microcap_rotation();
filtered_cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
filtered_cfg.signal_symbol = "000001.SZ".to_string();
filtered_cfg.refresh_rate = 99;
filtered_cfg.max_positions = 1;
@@ -36485,6 +36510,7 @@ mod tests {
fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.volume_capacity_mode = crate::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
cfg.signal_symbol = "000001.SZ".to_string();
cfg.rotation_enabled = false;
cfg.benchmark_short_ma_days = 1;