feat: introduce causal capacity primitives and exact participation quotas

This commit is contained in:
boris
2026-09-11 15:00:18 +08:00
parent fa0b316a8b
commit 4acecda79d
4 changed files with 242 additions and 70 deletions
+22 -12
View File
@@ -7,6 +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};
use crate::events::{
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
ProcessEventKind,
@@ -401,6 +402,7 @@ pub struct BrokerSimulator<C, R> {
execution_price_field: PriceField,
slippage_model: SlippageModel,
volume_percent: f64,
volume_rate: Result<ParticipationRate, CapacityError>,
volume_limit: bool,
inactive_limit: bool,
liquidity_limit: bool,
@@ -436,6 +438,7 @@ impl<C, R> BrokerSimulator<C, R> {
execution_price_field: PriceField::Open,
slippage_model: SlippageModel::None,
volume_percent: 0.25,
volume_rate: ParticipationRate::new(0.25),
volume_limit: true,
inactive_limit: true,
liquidity_limit: true,
@@ -475,6 +478,7 @@ impl<C, R> BrokerSimulator<C, R> {
execution_price_field,
slippage_model: SlippageModel::None,
volume_percent: 0.25,
volume_rate: ParticipationRate::new(0.25),
volume_limit: true,
inactive_limit: true,
liquidity_limit: true,
@@ -547,6 +551,7 @@ impl<C, R> BrokerSimulator<C, R> {
pub fn with_risk_config(mut self, config: FidcRiskControlConfig) -> Self {
self.volume_limit = config.trading_constraints.volume_limit_enabled;
self.volume_percent = config.trading_constraints.volume_percent;
self.volume_rate = ParticipationRate::new(self.volume_percent);
self.liquidity_limit = config.trading_constraints.liquidity_limit_enabled;
self.risk_config = config;
self
@@ -558,6 +563,7 @@ impl<C, R> BrokerSimulator<C, R> {
pub fn with_volume_percent(mut self, volume_percent: f64) -> Self {
self.volume_percent = volume_percent;
self.volume_rate = ParticipationRate::new(volume_percent);
self
}
@@ -1482,6 +1488,9 @@ where
data: &DataSet,
decision: &StrategyDecision,
) -> Result<BrokerExecutionReport, BacktestError> {
if self.volume_limit {
self.volume_rate.map_err(|error| BacktestError::Execution(error.to_string()))?;
}
let mut session = std::mem::take(&mut *self.execution_session.borrow_mut());
session.activate(date);
let result = self.execute_with_daily_session(date, portfolio, data, decision, &mut session);
@@ -7333,15 +7342,15 @@ where
}
if self.volume_limit {
let raw_limit = ((available_market_volume as f64) * self.volume_percent).floor() as i64
- consumed_turnover as i64;
if raw_limit <= 0 {
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 as u32
raw_limit
} else {
self.round_buy_quantity(raw_limit as u32, minimum_order_quantity, order_step_size)
self.round_buy_quantity(raw_limit, minimum_order_quantity, order_step_size)
};
if volume_limited == 0 {
return Err(volume_limit_reason.to_string());
@@ -7743,12 +7752,6 @@ where
remaining_qty
};
if self.volume_limit {
let raw_limit = ((quote.volume_delta as f64) * self.volume_percent).floor() as u32;
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)
};
let consumed = execution_ledger
.volume_consumed(symbol, quote.timestamp)
.saturating_add(
@@ -7757,7 +7760,14 @@ where
.copied()
.unwrap_or(0),
);
available_qty = available_qty.min(volume_limited.saturating_sub(consumed));
let raw_limit = self.volume_rate.map_err(|error| BacktestError::Execution(error.to_string()))?
.remaining(quote.volume_delta, u64::from(consumed), remaining_qty);
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)
};
available_qty = available_qty.min(volume_limited);
}
if available_qty == 0 {
continue;