fix: size TWAP slices from frozen clocks instead of future quotes
This commit is contained in:
@@ -8,6 +8,7 @@ use crate::cost::CostModel;
|
||||
use crate::data::{DataSet, IntradayExecutionQuote, PriceField};
|
||||
use crate::engine::BacktestError;
|
||||
use crate::execution_capacity::{CapacityError, ParticipationRate, VolumeObservation, VolumeObservationKind};
|
||||
use crate::execution_schedule::TwapSchedule;
|
||||
use crate::events::{
|
||||
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
||||
ProcessEventKind,
|
||||
@@ -7585,6 +7586,9 @@ where
|
||||
|
||||
let quote_quantity_limited =
|
||||
self.quote_quantity_limited_for_window(matching_type, start_cursor, end_cursor);
|
||||
let twap_schedule = (matching_type == MatchingType::Twap)
|
||||
.then(|| TwapSchedule::new(start_cursor, end_cursor, requested_qty))
|
||||
.transpose()?;
|
||||
let lot = round_lot.max(1);
|
||||
let exact_time_order_quote = matching_type != MatchingType::MinuteLast
|
||||
&& start_cursor.is_some()
|
||||
@@ -7634,7 +7638,7 @@ where
|
||||
let mut pending_volume_consumption = BTreeMap::<NaiveDateTime, u32>::new();
|
||||
let mut liquidity_consumption = Vec::new();
|
||||
|
||||
for (quote_index, quote) in eligible_quotes.iter().enumerate() {
|
||||
for quote in &eligible_quotes {
|
||||
let execution_at = if use_decision_time_quote {
|
||||
start_cursor.expect("as-of orders have an execution clock").max(quote.timestamp)
|
||||
} else {
|
||||
@@ -7724,11 +7728,8 @@ where
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut take_qty = if matching_type == MatchingType::Twap {
|
||||
let remaining_quotes = (eligible_quotes.len() - quote_index) as u32;
|
||||
let scheduled_qty =
|
||||
((remaining_qty as f64) / remaining_quotes.max(1) as f64).ceil() as u32;
|
||||
remaining_qty.min(available_qty).min(scheduled_qty.max(1))
|
||||
let mut take_qty = if let Some(schedule) = &twap_schedule {
|
||||
remaining_qty.min(available_qty).min(schedule.due_quantity(execution_at, filled_qty))
|
||||
} else {
|
||||
remaining_qty.min(available_qty)
|
||||
};
|
||||
@@ -8056,7 +8057,7 @@ mod tests {
|
||||
|
||||
use super::{
|
||||
BrokerExecutionReport, BrokerSimulator, EquityExecutionPhase, IntradayExecutionLedger,
|
||||
MatchingType, OpenOrder, RebalanceCashMode, SlippageModel,
|
||||
ExecutionFill, MatchingType, OpenOrder, RebalanceCashMode, SlippageModel,
|
||||
};
|
||||
use crate::cost::ChinaAShareCostModel;
|
||||
use crate::data::{
|
||||
@@ -11810,6 +11811,43 @@ mod tests {
|
||||
assert!((fill.price - 7.15428).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn twap_earlier_fills_do_not_depend_on_later_quote_liquidity_or_count() {
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_volume_limit(true).with_volume_percent(0.25).with_liquidity_limit(false);
|
||||
let snapshot = limit_test_snapshot();
|
||||
let start = snapshot.date.and_hms_opt(10, 0, 0).unwrap();
|
||||
let end = snapshot.date.and_hms_opt(10, 10, 0).unwrap();
|
||||
let quote_at = |minute| {
|
||||
let mut quote = limit_test_quote(10.0, 9.99, 10.01);
|
||||
quote.timestamp = snapshot.date.and_hms_opt(10, minute, 0).unwrap();
|
||||
quote.volume_delta = 10_000;
|
||||
quote.ask1_volume = 0;
|
||||
quote.bid1_volume = 0;
|
||||
quote
|
||||
};
|
||||
let original = vec![quote_at(2), quote_at(5), quote_at(10)];
|
||||
let mut changed = original.clone();
|
||||
changed[2].volume_delta = 0;
|
||||
let read = |quotes: &[IntradayExecutionQuote]| broker.select_execution_fill(
|
||||
&snapshot, quotes, OrderSide::Buy, MatchingType::Twap, Some(start), Some(end),
|
||||
1_000, 100, 100, 100, false, None, None, None,
|
||||
).unwrap();
|
||||
let original_fill = read(&original);
|
||||
let changed_fill = read(&changed);
|
||||
let fewer_fill = read(&original[..2]);
|
||||
let prefix = |fill: &ExecutionFill| fill.legs.iter()
|
||||
.filter(|leg| leg.execution_timestamp.unwrap() < end)
|
||||
.map(|leg| (leg.execution_timestamp, leg.quantity, leg.price.to_bits()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(prefix(&original_fill), prefix(&changed_fill));
|
||||
assert_eq!(prefix(&original_fill), prefix(&fewer_fill));
|
||||
assert_eq!(prefix(&original_fill).iter().map(|row| row.1).collect::<Vec<_>>(), vec![200, 300]);
|
||||
assert_eq!(original_fill.quantity, 1_000);
|
||||
assert_eq!(changed_fill.quantity, 500);
|
||||
assert_eq!(fewer_fill.quantity, 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instantaneous_twap_without_limits_does_not_cap_quote_quantity() {
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
|
||||
Reference in New Issue
Block a user