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

This commit is contained in:
boris
2026-06-13 21:27:21 +08:00
parent 0813ce3ffb
commit 89c2ff58f8
+122 -35
View File
@@ -367,15 +367,16 @@ where
.get() .get()
.or(self.intraday_execution_start_time) .or(self.intraday_execution_start_time)
.map(|start_time| date.and_time(start_time)); .map(|start_time| date.and_time(start_time));
if let Some(price) = data if let Some(price) = self
.execution_quotes_on(date, symbol) .latest_known_quote_at_or_before(
.iter() data.execution_quotes_on(date, symbol),
.filter(|quote| { start_cursor,
start_cursor snapshot,
.map(|cursor| quote.timestamp >= cursor) OrderSide::Buy,
.unwrap_or(true) MatchingType::NextTickLast,
}) false,
.find_map(|quote| { )
.and_then(|quote| {
(quote.last_price.is_finite() && quote.last_price > 0.0) (quote.last_price.is_finite() && quote.last_price > 0.0)
.then_some(quote.last_price) .then_some(quote.last_price)
}) })
@@ -399,22 +400,17 @@ where
.get() .get()
.or(self.intraday_execution_start_time) .or(self.intraday_execution_start_time)
.map(|start_time| date.and_time(start_time)); .map(|start_time| date.and_time(start_time));
data.execution_quotes_on(date, symbol) let matching_type = self.matching_type_for_algo_request(None);
.iter() self.latest_known_quote_at_or_before(
.filter(|quote| { data.execution_quotes_on(date, symbol),
start_cursor start_cursor,
.map(|cursor| quote.timestamp >= cursor) snapshot,
.unwrap_or(true) side,
}) matching_type,
.find_map(|quote| { false,
self.select_quote_reference_price( )
snapshot, .and_then(|quote| self.select_quote_reference_price(snapshot, quote, side, matching_type))
quote, .unwrap_or_else(|| self.sizing_price(snapshot))
side,
self.matching_type_for_algo_request(None),
)
})
.unwrap_or_else(|| self.sizing_price(snapshot))
} }
fn snapshot_execution_price( fn snapshot_execution_price(
@@ -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( fn process_limit_shares(
&self, &self,
date: NaiveDate, date: NaiveDate,
@@ -4654,15 +4680,31 @@ where
let quote_quantity_limited = let quote_quantity_limited =
self.quote_quantity_limited_for_window(matching_type, start_cursor, end_cursor); self.quote_quantity_limited_for_window(matching_type, start_cursor, end_cursor);
let lot = round_lot.max(1); let lot = round_lot.max(1);
let eligible_quotes: Vec<&IntradayExecutionQuote> = quotes let use_decision_time_quote = matching_type == MatchingType::NextTickLast
.iter() && start_cursor.is_some()
.filter(|quote| { && end_cursor.is_none_or(|end| start_cursor.is_some_and(|start| end <= start));
!start_cursor.is_some_and(|cursor| quote.timestamp < cursor) let eligible_quotes: Vec<&IntradayExecutionQuote> = if use_decision_time_quote {
&& !end_cursor.is_some_and(|cursor| quote.timestamp > cursor) self.latest_known_quote_at_or_before(
&& (!quote_quantity_limited quotes,
|| self.quote_has_executable_liquidity(quote, side, matching_type)) start_cursor,
}) snapshot,
.collect(); side,
matching_type,
quote_quantity_limited,
)
.into_iter()
.collect()
} else {
quotes
.iter()
.filter(|quote| {
!start_cursor.is_some_and(|cursor| quote.timestamp < cursor)
&& !end_cursor.is_some_and(|cursor| quote.timestamp > cursor)
&& (!quote_quantity_limited
|| self.quote_has_executable_liquidity(quote, side, matching_type))
})
.collect()
};
let mut filled_qty = 0_u32; let mut filled_qty = 0_u32;
let mut gross_amount = 0.0_f64; let mut gross_amount = 0.0_f64;
let mut last_timestamp = None; let mut last_timestamp = None;
@@ -5077,7 +5119,7 @@ mod tests {
snapshot.last_price = 10.0; snapshot.last_price = 10.0;
snapshot.close = 10.0; snapshot.close = 10.0;
let mut quote = limit_test_quote(11.0, 10.99, 11.01); 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( let data = DataSet::from_components_with_actions_and_quotes(
vec![limit_test_instrument()], vec![limit_test_instrument()],
vec![snapshot.clone()], 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] #[test]
fn instantaneous_twap_without_limits_does_not_cap_quote_quantity() { fn instantaneous_twap_without_limits_does_not_cap_quote_quantity() {
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks) let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)