Add futures depth matching and mod loader

This commit is contained in:
boris
2026-04-23 21:51:45 -07:00
parent ed8ac385e4
commit 895aee1388
7 changed files with 537 additions and 20 deletions
+83 -1
View File
@@ -7,7 +7,7 @@ use thiserror::Error;
use crate::broker::{BrokerExecutionReport, BrokerSimulator, MatchingType};
use crate::cost::CostModel;
use crate::data::{BenchmarkSnapshot, DataSet, DataSetError, PriceField};
use crate::event_bus::{BacktestProcessMod, ProcessEventBus};
use crate::event_bus::{BacktestProcessMod, BacktestProcessModLoader, ProcessEventBus};
use crate::events::{
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
ProcessEventKind,
@@ -401,6 +401,22 @@ impl<S, C, R> BacktestEngine<S, C, R> {
{
self.process_event_bus.install_mod(module);
}
pub fn install_process_mod_loader(
&mut self,
loader: &mut BacktestProcessModLoader,
) -> Vec<String> {
self.process_event_bus.install_mod_loader(loader)
}
pub fn install_enabled_process_mods(
&mut self,
loader: &mut BacktestProcessModLoader,
enabled_names: &[String],
) -> Vec<String> {
self.process_event_bus
.install_enabled_mods(loader, enabled_names)
}
}
impl<S, C, R> BacktestEngine<S, C, R>
@@ -1119,6 +1135,15 @@ where
intent: &FuturesOrderIntent,
) -> Option<(f64, u32)> {
let snapshot = self.data.market(date, &intent.symbol);
if matches!(
self.broker.matching_type(),
MatchingType::NextTickBestCounterparty | MatchingType::CounterpartyOffer
) {
let depth = self.data.order_book_depth_on(date, &intent.symbol);
if !depth.is_empty() {
return self.resolve_futures_depth_fill(date, intent, snapshot);
}
}
let quotes = self.data.execution_quotes_on(date, &intent.symbol);
for quote in quotes {
let price = match self.broker.matching_type() {
@@ -1172,6 +1197,63 @@ where
None
}
fn resolve_futures_depth_fill(
&self,
date: NaiveDate,
intent: &FuturesOrderIntent,
snapshot: Option<&crate::data::DailyMarketSnapshot>,
) -> Option<(f64, u32)> {
let depth = self.data.order_book_depth_on(date, &intent.symbol);
let mut cursor = 0usize;
while cursor < depth.len() {
let timestamp = depth[cursor].timestamp;
let start = cursor;
while cursor < depth.len() && depth[cursor].timestamp == timestamp {
cursor += 1;
}
let mut levels = depth[start..cursor].iter().collect::<Vec<_>>();
levels.sort_by(|left, right| left.level.cmp(&right.level));
let mut filled_quantity = 0_u32;
let mut gross_amount = 0.0_f64;
for level in levels {
let Some(price) = level.executable_price(intent.side()) else {
continue;
};
let can_trade = if let Some(snapshot) = snapshot {
self.futures_price_can_trade(snapshot, intent.side(), price, intent.limit_price)
} else {
futures_limit_satisfied(intent.side(), price, intent.limit_price)
};
if !can_trade {
if intent.limit_price.is_some() {
break;
}
continue;
}
let available_quantity =
level.executable_volume(intent.side()).min(u32::MAX as u64) as u32;
if available_quantity == 0 {
continue;
}
let remaining = intent.quantity.saturating_sub(filled_quantity);
if remaining == 0 {
break;
}
let take_quantity = remaining.min(available_quantity);
gross_amount += price * take_quantity as f64;
filled_quantity += take_quantity;
if filled_quantity >= intent.quantity {
break;
}
}
if filled_quantity > 0 {
return Some((gross_amount / filled_quantity as f64, filled_quantity));
}
}
None
}
fn futures_price_can_trade(
&self,
snapshot: &crate::data::DailyMarketSnapshot,