切换回测执行价为分钟线语义
This commit is contained in:
@@ -73,9 +73,9 @@ pub enum MatchingType {
|
||||
OpenAuction,
|
||||
CurrentBarClose,
|
||||
NextBarOpen,
|
||||
NextTickLast,
|
||||
NextTickBestOwn,
|
||||
NextTickBestCounterparty,
|
||||
NextMinuteLast,
|
||||
NextMinuteBestOwn,
|
||||
NextMinuteBestCounterparty,
|
||||
CounterpartyOffer,
|
||||
Vwap,
|
||||
Twap,
|
||||
@@ -563,7 +563,7 @@ where
|
||||
matching_type: MatchingType,
|
||||
) -> Option<f64> {
|
||||
let raw_price = match matching_type {
|
||||
MatchingType::NextTickBestOwn => match side {
|
||||
MatchingType::NextMinuteBestOwn => match side {
|
||||
OrderSide::Buy => {
|
||||
if quote.bid1.is_finite() && quote.bid1 > 0.0 {
|
||||
Some(quote.bid1)
|
||||
@@ -587,13 +587,13 @@ where
|
||||
}
|
||||
}
|
||||
},
|
||||
MatchingType::NextTickBestCounterparty | MatchingType::CounterpartyOffer => {
|
||||
MatchingType::NextMinuteBestCounterparty | MatchingType::CounterpartyOffer => {
|
||||
match side {
|
||||
OrderSide::Buy => quote.buy_price(),
|
||||
OrderSide::Sell => quote.sell_price(),
|
||||
}
|
||||
}
|
||||
MatchingType::NextTickLast | MatchingType::Vwap | MatchingType::Twap => {
|
||||
MatchingType::NextMinuteLast | MatchingType::Vwap | MatchingType::Twap => {
|
||||
if quote.last_price.is_finite() && quote.last_price > 0.0 {
|
||||
Some(quote.last_price)
|
||||
} else {
|
||||
@@ -4806,7 +4806,7 @@ where
|
||||
}
|
||||
|
||||
if self.inactive_limit && snapshot.tick_volume == 0 {
|
||||
return Err("tick no volume".to_string());
|
||||
return Err("minute no volume".to_string());
|
||||
}
|
||||
|
||||
let mut max_fill = requested_qty;
|
||||
@@ -4835,7 +4835,7 @@ where
|
||||
let raw_limit = ((snapshot.tick_volume as f64) * self.volume_percent).round() as i64
|
||||
- consumed_turnover as i64;
|
||||
if raw_limit <= 0 {
|
||||
return Err("tick volume limit".to_string());
|
||||
return Err("minute volume limit".to_string());
|
||||
}
|
||||
let volume_limited = if side == OrderSide::Sell && allow_odd_lot_sell {
|
||||
raw_limit as u32
|
||||
@@ -4843,7 +4843,7 @@ where
|
||||
self.round_buy_quantity(raw_limit as u32, minimum_order_quantity, order_step_size)
|
||||
};
|
||||
if volume_limited == 0 {
|
||||
return Err("tick volume limit".to_string());
|
||||
return Err("minute volume limit".to_string());
|
||||
}
|
||||
max_fill = max_fill.min(volume_limited);
|
||||
}
|
||||
@@ -4998,7 +4998,7 @@ where
|
||||
quotes,
|
||||
start_cursor,
|
||||
end_cursor,
|
||||
matching_type == MatchingType::NextTickLast && start_cursor.is_some(),
|
||||
matching_type == MatchingType::NextMinuteLast && start_cursor.is_some(),
|
||||
)),
|
||||
});
|
||||
}
|
||||
@@ -5056,12 +5056,12 @@ where
|
||||
let quote_quantity_limited =
|
||||
self.quote_quantity_limited_for_window(matching_type, start_cursor, end_cursor);
|
||||
let lot = round_lot.max(1);
|
||||
let exact_time_order_quote = matching_type != MatchingType::NextTickLast
|
||||
let exact_time_order_quote = matching_type != MatchingType::NextMinuteLast
|
||||
&& start_cursor.is_some()
|
||||
&& end_cursor.is_some()
|
||||
&& start_cursor == end_cursor;
|
||||
let use_decision_time_quote = start_cursor.is_some()
|
||||
&& (matching_type == MatchingType::NextTickLast || exact_time_order_quote);
|
||||
&& (matching_type == MatchingType::NextMinuteLast || exact_time_order_quote);
|
||||
let eligible_quotes: Vec<&IntradayExecutionQuote> = if use_decision_time_quote {
|
||||
self.latest_known_quote_at_or_before(
|
||||
quotes,
|
||||
@@ -5290,7 +5290,7 @@ where
|
||||
}
|
||||
|
||||
fn quote_quantity_limited(&self, matching_type: MatchingType) -> bool {
|
||||
self.volume_limit || self.liquidity_limit || matching_type != MatchingType::NextTickLast
|
||||
self.volume_limit || self.liquidity_limit || matching_type != MatchingType::NextMinuteLast
|
||||
}
|
||||
|
||||
fn quote_quantity_limited_for_window(
|
||||
@@ -5321,7 +5321,7 @@ fn matching_type_from_price_field(field: PriceField) -> MatchingType {
|
||||
PriceField::DayOpen => MatchingType::OpenAuction,
|
||||
PriceField::Open => MatchingType::NextBarOpen,
|
||||
PriceField::Close => MatchingType::CurrentBarClose,
|
||||
PriceField::Last => MatchingType::NextTickLast,
|
||||
PriceField::Last => MatchingType::NextMinuteLast,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5338,8 +5338,8 @@ fn merge_partial_fill_reason(current: Option<String>, next: Option<&str>) -> Opt
|
||||
|
||||
fn zero_fill_status_for_reason(reason: &str) -> OrderStatus {
|
||||
match reason {
|
||||
"tick no volume"
|
||||
| "tick volume limit"
|
||||
"minute no volume"
|
||||
| "minute volume limit"
|
||||
| "intraday quote liquidity exhausted"
|
||||
| "no execution quotes at or before start"
|
||||
| "no execution quotes after start"
|
||||
@@ -5487,17 +5487,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_tick_last_without_volume_or_liquidity_limit_does_not_cap_quote_quantity() {
|
||||
fn next_minute_last_without_volume_or_liquidity_limit_does_not_cap_quote_quantity() {
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(false);
|
||||
|
||||
assert!(!broker.quote_quantity_limited(MatchingType::NextTickLast));
|
||||
assert!(!broker.quote_quantity_limited(MatchingType::NextMinuteLast));
|
||||
assert!(broker.quote_quantity_limited(MatchingType::CounterpartyOffer));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_tick_last_keeps_quote_quantity_cap_when_limits_enabled() {
|
||||
fn next_minute_last_keeps_quote_quantity_cap_when_limits_enabled() {
|
||||
let volume_limited =
|
||||
BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_volume_limit(true)
|
||||
@@ -5507,8 +5507,8 @@ mod tests {
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(true);
|
||||
|
||||
assert!(volume_limited.quote_quantity_limited(MatchingType::NextTickLast));
|
||||
assert!(liquidity_limited.quote_quantity_limited(MatchingType::NextTickLast));
|
||||
assert!(volume_limited.quote_quantity_limited(MatchingType::NextMinuteLast));
|
||||
assert!(liquidity_limited.quote_quantity_limited(MatchingType::NextMinuteLast));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -5558,7 +5558,7 @@ mod tests {
|
||||
PriceField::Last,
|
||||
)
|
||||
.with_intraday_execution_start_time(date.and_hms_opt(10, 18, 0).unwrap().time())
|
||||
.with_matching_type(MatchingType::NextTickLast)
|
||||
.with_matching_type(MatchingType::NextMinuteLast)
|
||||
.with_slippage_model(SlippageModel::PriceRatio(0.001))
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(false);
|
||||
@@ -5751,7 +5751,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_tick_last_execution_uses_latest_quote_before_decision_time() {
|
||||
fn next_minute_last_execution_uses_latest_quote_before_decision_time() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
@@ -5772,7 +5772,7 @@ mod tests {
|
||||
&snapshot,
|
||||
&[quote],
|
||||
OrderSide::Sell,
|
||||
MatchingType::NextTickLast,
|
||||
MatchingType::NextMinuteLast,
|
||||
Some(decision_time),
|
||||
Some(decision_time),
|
||||
200,
|
||||
@@ -6026,7 +6026,7 @@ mod tests {
|
||||
&snapshot,
|
||||
&[quote],
|
||||
OrderSide::Buy,
|
||||
MatchingType::NextTickLast,
|
||||
MatchingType::NextMinuteLast,
|
||||
Some(start),
|
||||
None,
|
||||
100,
|
||||
@@ -6103,7 +6103,7 @@ mod tests {
|
||||
&snapshot,
|
||||
&[quote],
|
||||
OrderSide::Sell,
|
||||
MatchingType::NextTickLast,
|
||||
MatchingType::NextMinuteLast,
|
||||
Some(start),
|
||||
None,
|
||||
100,
|
||||
|
||||
Reference in New Issue
Block a user