修正点时刻回测使用最新tick

This commit is contained in:
boris
2026-06-13 21:27:21 +08:00
parent 0813ce3ffb
commit 89c2ff58f8
+111 -24
View File
@@ -367,15 +367,16 @@ where
.get()
.or(self.intraday_execution_start_time)
.map(|start_time| date.and_time(start_time));
if let Some(price) = data
.execution_quotes_on(date, symbol)
.iter()
.filter(|quote| {
start_cursor
.map(|cursor| quote.timestamp >= cursor)
.unwrap_or(true)
})
.find_map(|quote| {
if let Some(price) = self
.latest_known_quote_at_or_before(
data.execution_quotes_on(date, symbol),
start_cursor,
snapshot,
OrderSide::Buy,
MatchingType::NextTickLast,
false,
)
.and_then(|quote| {
(quote.last_price.is_finite() && quote.last_price > 0.0)
.then_some(quote.last_price)
})
@@ -399,21 +400,16 @@ where
.get()
.or(self.intraday_execution_start_time)
.map(|start_time| date.and_time(start_time));
data.execution_quotes_on(date, symbol)
.iter()
.filter(|quote| {
start_cursor
.map(|cursor| quote.timestamp >= cursor)
.unwrap_or(true)
})
.find_map(|quote| {
self.select_quote_reference_price(
let matching_type = self.matching_type_for_algo_request(None);
self.latest_known_quote_at_or_before(
data.execution_quotes_on(date, symbol),
start_cursor,
snapshot,
quote,
side,
self.matching_type_for_algo_request(None),
matching_type,
false,
)
})
.and_then(|quote| self.select_quote_reference_price(snapshot, quote, side, matching_type))
.unwrap_or_else(|| self.sizing_price(snapshot))
}
@@ -1143,6 +1139,36 @@ where
}
}
fn latest_known_quote_at_or_before<'a>(
&self,
quotes: &'a [IntradayExecutionQuote],
cursor: Option<NaiveDateTime>,
snapshot: &crate::data::DailyMarketSnapshot,
side: OrderSide,
matching_type: MatchingType,
require_executable_liquidity: bool,
) -> Option<&'a IntradayExecutionQuote> {
let Some(cursor) = cursor else {
return quotes.iter().find(|quote| {
self.select_quote_reference_price(snapshot, quote, side, matching_type)
.is_some()
&& (!require_executable_liquidity
|| self.quote_has_executable_liquidity(quote, side, matching_type))
});
};
quotes
.iter()
.filter(|quote| {
quote.timestamp <= cursor
&& self
.select_quote_reference_price(snapshot, quote, side, matching_type)
.is_some()
&& (!require_executable_liquidity
|| self.quote_has_executable_liquidity(quote, side, matching_type))
})
.max_by_key(|quote| quote.timestamp)
}
fn process_limit_shares(
&self,
date: NaiveDate,
@@ -4654,7 +4680,22 @@ where
let quote_quantity_limited =
self.quote_quantity_limited_for_window(matching_type, start_cursor, end_cursor);
let lot = round_lot.max(1);
let eligible_quotes: Vec<&IntradayExecutionQuote> = quotes
let use_decision_time_quote = matching_type == MatchingType::NextTickLast
&& start_cursor.is_some()
&& end_cursor.is_none_or(|end| start_cursor.is_some_and(|start| end <= start));
let eligible_quotes: Vec<&IntradayExecutionQuote> = if use_decision_time_quote {
self.latest_known_quote_at_or_before(
quotes,
start_cursor,
snapshot,
side,
matching_type,
quote_quantity_limited,
)
.into_iter()
.collect()
} else {
quotes
.iter()
.filter(|quote| {
!start_cursor.is_some_and(|cursor| quote.timestamp < cursor)
@@ -4662,7 +4703,8 @@ where
&& (!quote_quantity_limited
|| self.quote_has_executable_liquidity(quote, side, matching_type))
})
.collect();
.collect()
};
let mut filled_qty = 0_u32;
let mut gross_amount = 0.0_f64;
let mut last_timestamp = None;
@@ -5077,7 +5119,7 @@ mod tests {
snapshot.last_price = 10.0;
snapshot.close = 10.0;
let mut quote = limit_test_quote(11.0, 10.99, 11.01);
quote.timestamp = date.and_hms_opt(9, 34, 0).expect("valid timestamp");
quote.timestamp = date.and_hms_opt(9, 32, 58).expect("valid timestamp");
let data = DataSet::from_components_with_actions_and_quotes(
vec![limit_test_instrument()],
vec![snapshot.clone()],
@@ -5100,6 +5142,51 @@ mod tests {
);
}
#[test]
fn next_tick_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(),
ChinaEquityRuleHooks,
PriceField::Last,
)
.with_volume_limit(false)
.with_liquidity_limit(false)
.with_inactive_limit(false);
let snapshot = limit_test_snapshot();
let mut quote = limit_test_quote(10.8, 10.79, 10.81);
quote.timestamp = date.and_hms_opt(9, 32, 58).expect("valid timestamp");
let quote_timestamp = quote.timestamp;
let decision_time = date.and_hms_opt(9, 33, 0).expect("valid timestamp");
let fill = broker
.select_execution_fill(
&snapshot,
&[quote],
OrderSide::Sell,
MatchingType::NextTickLast,
Some(decision_time),
Some(decision_time),
200,
100,
100,
100,
false,
None,
None,
None,
)
.expect("fill from latest known quote before decision time");
assert_eq!(fill.quantity, 200);
assert_eq!(fill.legs.len(), 1);
assert_eq!(fill.legs[0].price, 10.8);
assert_eq!(
fill.next_cursor,
quote_timestamp + chrono::Duration::seconds(1)
);
}
#[test]
fn instantaneous_twap_without_limits_does_not_cap_quote_quantity() {
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)