feat: introduce causal capacity primitives and exact participation quotas
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user