修复日线撮合误用分钟成交量
This commit is contained in:
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user