fix: separate historical session capacity audits from execution sizing
This commit is contained in:
@@ -7,7 +7,7 @@ use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime};
|
||||
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_capacity::{CapacityAuditSummary, CapacityError, ParticipationRate, SessionCapacityAudit, VolumeCapacityMode, VolumeObservation, VolumeObservationKind};
|
||||
use crate::execution_schedule::TwapSchedule;
|
||||
use crate::events::{
|
||||
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
||||
@@ -423,6 +423,7 @@ pub struct BrokerSimulator<C, R> {
|
||||
volume_percent: f64,
|
||||
volume_rate: Result<ParticipationRate, CapacityError>,
|
||||
volume_limit: bool,
|
||||
volume_capacity_mode: VolumeCapacityMode,
|
||||
inactive_limit: bool,
|
||||
liquidity_limit: bool,
|
||||
strict_value_budget: bool,
|
||||
@@ -459,6 +460,7 @@ impl<C, R> BrokerSimulator<C, R> {
|
||||
volume_percent: 0.25,
|
||||
volume_rate: ParticipationRate::new(0.25),
|
||||
volume_limit: true,
|
||||
volume_capacity_mode: VolumeCapacityMode::ExecutionObservation,
|
||||
inactive_limit: true,
|
||||
liquidity_limit: true,
|
||||
strict_value_budget: true,
|
||||
@@ -499,6 +501,7 @@ impl<C, R> BrokerSimulator<C, R> {
|
||||
volume_percent: 0.25,
|
||||
volume_rate: ParticipationRate::new(0.25),
|
||||
volume_limit: true,
|
||||
volume_capacity_mode: VolumeCapacityMode::ExecutionObservation,
|
||||
inactive_limit: true,
|
||||
liquidity_limit: true,
|
||||
strict_value_budget: true,
|
||||
@@ -529,6 +532,29 @@ impl<C, R> BrokerSimulator<C, R> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_volume_capacity_mode(mut self, mode: VolumeCapacityMode) -> Self {
|
||||
self.volume_capacity_mode = mode;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn capacity_audit_summary(&self) -> CapacityAuditSummary {
|
||||
CapacityAuditSummary { mode: self.volume_capacity_mode, enabled: self.volume_limit,
|
||||
participation_rate: self.volume_percent, ..Default::default() }
|
||||
}
|
||||
|
||||
pub fn audit_completed_session_capacity(&self, date: NaiveDate, data: &DataSet) -> Result<Vec<SessionCapacityAudit>, BacktestError> {
|
||||
if !self.volume_limit || self.volume_capacity_mode != VolumeCapacityMode::SessionCapacityAudit {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let session = self.execution_session.borrow();
|
||||
if session.date != Some(date) { return Ok(Vec::new()); }
|
||||
let rate = self.volume_rate.map_err(|error| BacktestError::Execution(error.to_string()))?;
|
||||
session.intraday_turnover.iter().filter(|(_, quantity)| **quantity > 0).map(|(symbol, quantity)| {
|
||||
let market = data.market(date, symbol).ok_or_else(|| BacktestError::MissingPrice { date, symbol: symbol.clone(), field: "session capacity audit" })?;
|
||||
Ok(SessionCapacityAudit::new(date, symbol.clone(), u64::from(*quantity), market.volume, rate))
|
||||
}).collect()
|
||||
}
|
||||
|
||||
pub fn with_inactive_limit(mut self, enabled: bool) -> Self {
|
||||
self.inactive_limit = enabled;
|
||||
self
|
||||
@@ -1458,6 +1484,8 @@ where
|
||||
) -> Result<BrokerExecutionReport, BacktestError> {
|
||||
if self.volume_limit {
|
||||
self.volume_rate.map_err(|error| BacktestError::Execution(error.to_string()))?;
|
||||
self.volume_capacity_mode.validate(true, self.matching_type_uses_intraday_quotes())
|
||||
.map_err(|error| BacktestError::Execution(error.to_string()))?;
|
||||
}
|
||||
let mut session = std::mem::take(&mut *self.execution_session.borrow_mut());
|
||||
session.activate(date);
|
||||
@@ -7269,57 +7297,13 @@ where
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let uses_intraday_quantity = self.matching_type_uses_intraday_quotes();
|
||||
let available_market_volume = if uses_intraday_quantity {
|
||||
snapshot.minute_volume
|
||||
} else {
|
||||
snapshot.volume
|
||||
};
|
||||
let no_volume_reason = if uses_intraday_quantity {
|
||||
"minute no volume"
|
||||
} else {
|
||||
"daily no volume"
|
||||
};
|
||||
let volume_limit_reason = if uses_intraday_quantity {
|
||||
"minute volume limit"
|
||||
} else {
|
||||
"daily volume limit"
|
||||
};
|
||||
|
||||
let mut max_fill = requested_qty;
|
||||
|
||||
if self.inactive_limit
|
||||
&& (snapshot.paused || (!uses_intraday_quantity && available_market_volume == 0))
|
||||
{
|
||||
return Err(if snapshot.paused {
|
||||
"paused".to_string()
|
||||
} else {
|
||||
no_volume_reason.to_string()
|
||||
});
|
||||
}
|
||||
|
||||
if uses_intraday_quantity {
|
||||
return Ok(max_fill);
|
||||
}
|
||||
|
||||
if self.volume_limit {
|
||||
let raw_limit = self.volume_rate.map_err(|error| error.to_string())?
|
||||
.remaining(available_market_volume, u64::from(consumed_turnover), requested_qty);
|
||||
if raw_limit == 0 {
|
||||
return Err(volume_limit_reason.to_string());
|
||||
}
|
||||
let volume_limited = if side == OrderSide::Sell && allow_odd_lot_sell {
|
||||
raw_limit
|
||||
} else {
|
||||
self.round_buy_quantity(raw_limit, minimum_order_quantity, order_step_size)
|
||||
};
|
||||
if volume_limited == 0 {
|
||||
return Err(volume_limit_reason.to_string());
|
||||
}
|
||||
max_fill = max_fill.min(volume_limited);
|
||||
}
|
||||
|
||||
Ok(max_fill)
|
||||
let _ = (side, minimum_order_quantity, order_step_size, consumed_turnover, allow_odd_lot_sell);
|
||||
if self.inactive_limit && snapshot.paused { return Err("paused".into()); }
|
||||
self.volume_capacity_mode.validate(self.volume_limit, self.matching_type_uses_intraday_quotes())
|
||||
.map_err(|error| error.to_string())?;
|
||||
// Per-observation limits are applied to each actual quote below. The
|
||||
// session-audit model must never size this order from the day's total.
|
||||
Ok(requested_qty)
|
||||
}
|
||||
|
||||
fn price_satisfies_limit(
|
||||
@@ -7723,7 +7707,7 @@ where
|
||||
} else {
|
||||
remaining_qty
|
||||
};
|
||||
if self.volume_limit {
|
||||
if self.volume_limit && self.volume_capacity_mode.limits_execution_quantity() {
|
||||
let consumed = execution_ledger
|
||||
.volume_consumed(symbol, quote.timestamp)
|
||||
.saturating_add(
|
||||
@@ -7871,7 +7855,7 @@ where
|
||||
.saturating_add(take_qty)
|
||||
.min(state.displayed_quantity);
|
||||
}
|
||||
if self.volume_limit {
|
||||
if self.volume_limit && self.volume_capacity_mode.limits_execution_quantity() {
|
||||
let consumed = pending_volume_consumption
|
||||
.entry(quote.timestamp)
|
||||
.or_default();
|
||||
@@ -7885,7 +7869,7 @@ where
|
||||
depth_price_bits,
|
||||
displayed_quantity,
|
||||
consume_depth,
|
||||
consume_volume: self.volume_limit,
|
||||
consume_volume: self.volume_limit && self.volume_capacity_mode.limits_execution_quantity(),
|
||||
quantity: take_qty,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user