修复日线撮合误用分钟成交量

This commit is contained in:
boris
2026-07-02 21:12:48 +08:00
parent f796a85617
commit 97931c3766
+103 -7
View File
@@ -5038,12 +5038,33 @@ where
return Ok(0); return Ok(0);
} }
if self.inactive_limit && snapshot.minute_volume == 0 { let uses_intraday_quantity = self.matching_type_uses_intraday_quotes();
return Err("minute no volume".to_string()); let available_market_volume = if uses_intraday_quantity {
snapshot.minute_volume
} else {
snapshot.volume
};
let no_volume_reason = if uses_intraday_quantity {
"minute no volume"
} else {
"daily no volume"
};
let volume_limit_reason = if uses_intraday_quantity {
"minute volume limit"
} else {
"daily volume limit"
};
if self.inactive_limit && (snapshot.paused || available_market_volume == 0) {
return Err(if snapshot.paused {
"paused".to_string()
} else {
no_volume_reason.to_string()
});
} }
let mut max_fill = requested_qty; let mut max_fill = requested_qty;
if self.liquidity_limit && !self.is_open_auction_matching() { if self.liquidity_limit && uses_intraday_quantity && !self.is_open_auction_matching() {
let top_level_liquidity = match side { let top_level_liquidity = match side {
OrderSide::Buy => snapshot.liquidity_for_buy(), OrderSide::Buy => snapshot.liquidity_for_buy(),
OrderSide::Sell => snapshot.liquidity_for_sell(), OrderSide::Sell => snapshot.liquidity_for_sell(),
@@ -5065,10 +5086,10 @@ where
} }
if self.volume_limit { if self.volume_limit {
let raw_limit = ((snapshot.minute_volume as f64) * self.volume_percent).round() as i64 let raw_limit = ((available_market_volume as f64) * self.volume_percent).round() as i64
- consumed_turnover as i64; - consumed_turnover as i64;
if raw_limit <= 0 { if raw_limit <= 0 {
return Err("minute volume limit".to_string()); return Err(volume_limit_reason.to_string());
} }
let volume_limited = if side == OrderSide::Sell && allow_odd_lot_sell { let volume_limited = if side == OrderSide::Sell && allow_odd_lot_sell {
raw_limit as u32 raw_limit as u32
@@ -5076,7 +5097,7 @@ where
self.round_buy_quantity(raw_limit as u32, minimum_order_quantity, order_step_size) self.round_buy_quantity(raw_limit as u32, minimum_order_quantity, order_step_size)
}; };
if volume_limited == 0 { if volume_limited == 0 {
return Err("minute volume limit".to_string()); return Err(volume_limit_reason.to_string());
} }
max_fill = max_fill.min(volume_limited); max_fill = max_fill.min(volume_limited);
} }
@@ -5528,8 +5549,28 @@ where
} }
} }
fn matching_type_uses_intraday_quotes(&self) -> bool {
matches!(
self.matching_type,
MatchingType::MinuteLast
| MatchingType::MinuteBestOwn
| MatchingType::MinuteBestCounterparty
| MatchingType::Vwap
| MatchingType::Twap
)
}
fn quote_quantity_limited(&self, matching_type: MatchingType) -> bool { fn quote_quantity_limited(&self, matching_type: MatchingType) -> bool {
self.volume_limit || self.liquidity_limit || matching_type != MatchingType::MinuteLast match matching_type {
MatchingType::OpenAuction
| MatchingType::CurrentBarClose
| MatchingType::NextBarOpen => false,
MatchingType::MinuteLast => self.volume_limit || self.liquidity_limit,
MatchingType::MinuteBestOwn
| MatchingType::MinuteBestCounterparty
| MatchingType::Vwap
| MatchingType::Twap => true,
}
} }
fn quote_quantity_limited_for_window( fn quote_quantity_limited_for_window(
@@ -5750,6 +5791,61 @@ mod tests {
assert!(liquidity_limited.quote_quantity_limited(MatchingType::MinuteLast)); assert!(liquidity_limited.quote_quantity_limited(MatchingType::MinuteLast));
} }
#[test]
fn daily_matching_does_not_require_intraday_quote_quantity() {
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
.with_volume_limit(true)
.with_liquidity_limit(true);
assert!(!broker.quote_quantity_limited(MatchingType::OpenAuction));
assert!(!broker.quote_quantity_limited(MatchingType::CurrentBarClose));
assert!(!broker.quote_quantity_limited(MatchingType::NextBarOpen));
assert!(broker.quote_quantity_limited(MatchingType::MinuteLast));
assert!(broker.quote_quantity_limited(MatchingType::MinuteBestCounterparty));
}
#[test]
fn current_bar_close_volume_limit_uses_daily_volume_when_minute_volume_missing() {
let mut snapshot = limit_test_snapshot();
snapshot.minute_volume = 0;
snapshot.volume = 1_000_000;
snapshot.bid1_volume = 0;
snapshot.ask1_volume = 0;
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Close,
)
.with_matching_type(MatchingType::CurrentBarClose)
.with_volume_limit(true)
.with_liquidity_limit(true);
let fillable =
broker.market_fillable_quantity(&snapshot, OrderSide::Buy, 5_000, 100, 100, 0, false);
assert_eq!(fillable, Ok(5_000));
}
#[test]
fn current_bar_close_volume_limit_rejects_daily_zero_volume() {
let mut snapshot = limit_test_snapshot();
snapshot.minute_volume = 0;
snapshot.volume = 0;
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Close,
)
.with_matching_type(MatchingType::CurrentBarClose)
.with_volume_limit(true)
.with_liquidity_limit(false);
let fillable =
broker.market_fillable_quantity(&snapshot, OrderSide::Buy, 5_000, 100, 100, 0, false);
assert_eq!(fillable, Err("daily no volume".to_string()));
}
#[test] #[test]
fn target_value_valuation_uses_daily_snapshot_but_value_order_sizing_uses_intraday_minute() { fn target_value_valuation_uses_daily_snapshot_but_value_order_sizing_uses_intraday_minute() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date"); let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");