修复日线撮合误用分钟成交量
This commit is contained in:
@@ -5038,12 +5038,33 @@ where
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
if self.inactive_limit && snapshot.minute_volume == 0 {
|
||||
return Err("minute no volume".to_string());
|
||||
let uses_intraday_quantity = self.matching_type_uses_intraday_quotes();
|
||||
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;
|
||||
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 {
|
||||
OrderSide::Buy => snapshot.liquidity_for_buy(),
|
||||
OrderSide::Sell => snapshot.liquidity_for_sell(),
|
||||
@@ -5065,10 +5086,10 @@ where
|
||||
}
|
||||
|
||||
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;
|
||||
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 {
|
||||
raw_limit as u32
|
||||
@@ -5076,7 +5097,7 @@ where
|
||||
self.round_buy_quantity(raw_limit as u32, minimum_order_quantity, order_step_size)
|
||||
};
|
||||
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);
|
||||
}
|
||||
@@ -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 {
|
||||
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(
|
||||
@@ -5750,6 +5791,61 @@ mod tests {
|
||||
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]
|
||||
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");
|
||||
|
||||
Reference in New Issue
Block a user