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::cost::CostModel;
use crate::data::{DataSet, IntradayExecutionQuote, PriceField}; use crate::data::{DataSet, IntradayExecutionQuote, PriceField};
use crate::engine::BacktestError; use crate::engine::BacktestError;
use crate::execution_capacity::{CapacityError, ParticipationRate};
use crate::events::{ use crate::events::{
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent, AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
ProcessEventKind, ProcessEventKind,
@@ -401,6 +402,7 @@ pub struct BrokerSimulator<C, R> {
execution_price_field: PriceField, execution_price_field: PriceField,
slippage_model: SlippageModel, slippage_model: SlippageModel,
volume_percent: f64, volume_percent: f64,
volume_rate: Result<ParticipationRate, CapacityError>,
volume_limit: bool, volume_limit: bool,
inactive_limit: bool, inactive_limit: bool,
liquidity_limit: bool, liquidity_limit: bool,
@@ -436,6 +438,7 @@ impl<C, R> BrokerSimulator<C, R> {
execution_price_field: PriceField::Open, execution_price_field: PriceField::Open,
slippage_model: SlippageModel::None, slippage_model: SlippageModel::None,
volume_percent: 0.25, volume_percent: 0.25,
volume_rate: ParticipationRate::new(0.25),
volume_limit: true, volume_limit: true,
inactive_limit: true, inactive_limit: true,
liquidity_limit: true, liquidity_limit: true,
@@ -475,6 +478,7 @@ impl<C, R> BrokerSimulator<C, R> {
execution_price_field, execution_price_field,
slippage_model: SlippageModel::None, slippage_model: SlippageModel::None,
volume_percent: 0.25, volume_percent: 0.25,
volume_rate: ParticipationRate::new(0.25),
volume_limit: true, volume_limit: true,
inactive_limit: true, inactive_limit: true,
liquidity_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 { pub fn with_risk_config(mut self, config: FidcRiskControlConfig) -> Self {
self.volume_limit = config.trading_constraints.volume_limit_enabled; self.volume_limit = config.trading_constraints.volume_limit_enabled;
self.volume_percent = config.trading_constraints.volume_percent; 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.liquidity_limit = config.trading_constraints.liquidity_limit_enabled;
self.risk_config = config; self.risk_config = config;
self self
@@ -558,6 +563,7 @@ impl<C, R> BrokerSimulator<C, R> {
pub fn with_volume_percent(mut self, volume_percent: f64) -> Self { pub fn with_volume_percent(mut self, volume_percent: f64) -> Self {
self.volume_percent = volume_percent; self.volume_percent = volume_percent;
self.volume_rate = ParticipationRate::new(volume_percent);
self self
} }
@@ -1482,6 +1488,9 @@ where
data: &DataSet, data: &DataSet,
decision: &StrategyDecision, decision: &StrategyDecision,
) -> Result<BrokerExecutionReport, BacktestError> { ) -> 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()); let mut session = std::mem::take(&mut *self.execution_session.borrow_mut());
session.activate(date); session.activate(date);
let result = self.execute_with_daily_session(date, portfolio, data, decision, &mut session); let result = self.execute_with_daily_session(date, portfolio, data, decision, &mut session);
@@ -7333,15 +7342,15 @@ where
} }
if self.volume_limit { if self.volume_limit {
let raw_limit = ((available_market_volume as f64) * self.volume_percent).floor() as i64 let raw_limit = self.volume_rate.map_err(|error| error.to_string())?
- consumed_turnover as i64; .remaining(available_market_volume, u64::from(consumed_turnover), requested_qty);
if raw_limit <= 0 { if raw_limit == 0 {
return Err(volume_limit_reason.to_string()); return Err(volume_limit_reason.to_string());
} }
let volume_limited = if side == OrderSide::Sell && allow_odd_lot_sell { let volume_limited = if side == OrderSide::Sell && allow_odd_lot_sell {
raw_limit as u32 raw_limit
} else { } 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 { if volume_limited == 0 {
return Err(volume_limit_reason.to_string()); return Err(volume_limit_reason.to_string());
@@ -7743,12 +7752,6 @@ where
remaining_qty remaining_qty
}; };
if self.volume_limit { 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 let consumed = execution_ledger
.volume_consumed(symbol, quote.timestamp) .volume_consumed(symbol, quote.timestamp)
.saturating_add( .saturating_add(
@@ -7757,7 +7760,14 @@ where
.copied() .copied()
.unwrap_or(0), .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 { if available_qty == 0 {
continue; continue;
+170
View File
@@ -0,0 +1,170 @@
//! Causal volume budgets. Session totals may audit fills, never size earlier orders.
use chrono::{NaiveDate, NaiveDateTime};
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VolumeCapacityMode {
#[default]
ExecutionObservation,
CompletedBar,
SessionCapacityAudit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum CapacityError {
#[error("execution capacity ratio must be finite and in (0, 1]")]
InvalidRatio,
#[error("execution capacity decimal cannot be represented exactly")]
InvalidDecimal,
#[error("execution capacity observation has invalid time bounds")]
InvalidWindow,
#[error("execution capacity is not visible: available={available_at}, execution={execution_at}")]
NotVisible { available_at: NaiveDateTime, execution_at: NaiveDateTime },
#[error("execution capacity observation belongs to another session")]
WrongSession,
#[error("execution-time capacity is missing; daily session volume cannot size an earlier fill")]
MissingObservation,
}
/// Decimal semantics of the frozen JSON rate, evaluated without a float product.
#[derive(Debug, Clone, Copy)]
pub struct ParticipationRate {
numerator: u128,
denominator: u128,
}
impl ParticipationRate {
pub fn new(rate: f64) -> Result<Self, CapacityError> {
if !rate.is_finite() || rate <= 0.0 || rate > 1.0 {
return Err(CapacityError::InvalidRatio);
}
if rate < 1e-20 {
// Even u64::MAX shares at this rate cannot admit a single share.
return Ok(Self { numerator: 0, denominator: 1 });
}
if rate == 1.0 {
return Ok(Self { numerator: 1, denominator: 1 });
}
let text = rate.to_string();
let digits = text.strip_prefix("0.").ok_or(CapacityError::InvalidDecimal)?;
let digits = digits.trim_end_matches('0');
let numerator = digits.parse::<u128>().map_err(|_| CapacityError::InvalidDecimal)?;
let denominator = 10_u128.checked_pow(digits.len() as u32).ok_or(CapacityError::InvalidDecimal)?;
if numerator > u128::MAX / u128::from(u64::MAX) {
return Err(CapacityError::InvalidDecimal);
}
Ok(Self { numerator, denominator })
}
pub fn total_shares(self, market_shares: u64) -> u64 {
let total = u128::from(market_shares) * self.numerator / self.denominator;
u64::try_from(total).expect("participation rate cannot exceed the market shares")
}
pub fn remaining(self, market_shares: u64, consumed_shares: u64, requested: u32) -> u32 {
self.total_shares(market_shares).saturating_sub(consumed_shares).min(u64::from(requested)) as u32
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VolumeObservationKind {
TradeIncrement,
CompletedBar,
CumulativeSession,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct VolumeObservation {
pub kind: VolumeObservationKind,
pub start: NaiveDateTime,
pub end: NaiveDateTime,
pub available_at: NaiveDateTime,
pub shares: u64,
}
impl VolumeObservation {
pub fn visible_shares(self, execution_at: NaiveDateTime) -> Result<u64, CapacityError> {
if self.start > self.end || self.available_at < self.end {
return Err(CapacityError::InvalidWindow);
}
if self.available_at > execution_at {
return Err(CapacityError::NotVisible { available_at: self.available_at, execution_at });
}
if self.start.date() != self.end.date() || self.end.date() != execution_at.date() {
return Err(CapacityError::WrongSession);
}
Ok(self.shares)
}
pub fn remaining(self, execution_at: NaiveDateTime, rate: ParticipationRate, consumed: u64, requested: u32) -> Result<u32, CapacityError> {
Ok(rate.remaining(self.visible_shares(execution_at)?, consumed, requested))
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SessionCapacityAudit {
pub date: NaiveDate,
pub symbol: String,
pub filled_shares: u64,
pub session_shares: u64,
pub allowed_shares: u64,
pub passed: bool,
}
impl SessionCapacityAudit {
pub fn new(date: NaiveDate, symbol: String, filled_shares: u64, session_shares: u64, rate: ParticipationRate) -> Self {
let allowed_shares = rate.total_shares(session_shares);
Self { date, symbol, filled_shares, session_shares, allowed_shares, passed: filled_shares <= allowed_shares }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decimal_participation_never_rounds_a_fractional_share_up_or_overflows() {
assert_eq!(ParticipationRate::new(0.58).unwrap().total_shares(50), 29);
assert_eq!(ParticipationRate::new(0.25).unwrap().total_shares(3), 0);
assert_eq!(ParticipationRate::new(0.5).unwrap().total_shares(3), 1);
assert_eq!(ParticipationRate::new(1.).unwrap().total_shares(u64::MAX), u64::MAX);
assert_eq!(ParticipationRate::new(0.25).unwrap().remaining(u64::MAX, 0, u32::MAX), u32::MAX);
assert_eq!(ParticipationRate::new(f64::MIN_POSITIVE).unwrap().total_shares(u64::MAX), 0);
for rate in [0., -1., f64::NAN, f64::INFINITY, 1.001] {
assert!(ParticipationRate::new(rate).is_err());
}
}
#[test]
fn completed_volume_cannot_be_used_for_an_earlier_open() {
let day = NaiveDate::from_ymd_opt(2025,1,2).unwrap();
let opening = day.and_hms_opt(9,30,0).unwrap();
let closing = day.and_hms_opt(15,0,0).unwrap();
let observation = VolumeObservation { kind:VolumeObservationKind::CompletedBar, start:opening, end:closing, available_at:closing, shares:10000 };
assert!(matches!(observation.visible_shares(opening), Err(CapacityError::NotVisible { .. })));
assert_eq!(observation.remaining(closing, ParticipationRate::new(0.25).unwrap(), 1000, 5000).unwrap(), 1500);
assert!(matches!(observation.visible_shares(closing+chrono::Duration::days(1)), Err(CapacityError::WrongSession)));
}
#[test]
fn delayed_publication_and_invalid_bounds_are_not_treated_as_zero_volume() {
let at = NaiveDate::from_ymd_opt(2025,1,2).unwrap().and_hms_opt(10,18,0).unwrap();
let observation = VolumeObservation { kind:VolumeObservationKind::TradeIncrement, start:at, end:at, available_at:at+chrono::Duration::seconds(1), shares:0 };
assert!(matches!(observation.visible_shares(at), Err(CapacityError::NotVisible { .. })));
assert_eq!(VolumeObservation { available_at:at-chrono::Duration::seconds(1), ..observation }.visible_shares(at), Err(CapacityError::InvalidWindow));
assert_eq!(VolumeObservation { available_at:at, ..observation }.visible_shares(at).unwrap(), 0);
}
#[test]
fn session_audit_changes_verdict_not_executed_quantity() {
let day = NaiveDate::from_ymd_opt(2025,1,2).unwrap();
let rate = ParticipationRate::new(0.25).unwrap();
let a = SessionCapacityAudit::new(day,"TEST".into(),1000,3000,rate);
let b = SessionCapacityAudit::new(day,"TEST".into(),1000,5000,rate);
assert!(!a.passed); assert!(b.passed);
assert_eq!(a.filled_shares,b.filled_shares);
}
}
+1
View File
@@ -6,6 +6,7 @@ pub mod daily_patterns;
pub mod pattern_context; pub mod pattern_context;
pub mod session_events; pub mod session_events;
pub mod factor_events; pub mod factor_events;
pub mod execution_capacity;
mod factor_event_catalog; mod factor_event_catalog;
pub mod factor_cross_section; pub mod factor_cross_section;
pub mod market_event_context; pub mod market_event_context;
+49 -58
View File
@@ -17,6 +17,7 @@ use crate::data::{
decision_market_cap_bn, decision_market_cap_bn,
}; };
use crate::engine::BacktestError; use crate::engine::BacktestError;
use crate::execution_capacity::{CapacityError, ParticipationRate};
use crate::events::{OrderSide, ProcessEvent, ProcessEventKind}; use crate::events::{OrderSide, ProcessEvent, ProcessEventKind};
use crate::fixed_point::FixedMoney; use crate::fixed_point::FixedMoney;
use crate::futures::{ use crate::futures::{
@@ -1369,6 +1370,7 @@ pub struct PlatformExprStrategy {
pattern_specs: RefCell<BTreeMap<String,String>>, pattern_specs: RefCell<BTreeMap<String,String>>,
pattern_frame_at:RefCell<Option<NaiveDateTime>>, pattern_frame_at:RefCell<Option<NaiveDateTime>>,
config: PlatformExprStrategyConfig, config: PlatformExprStrategyConfig,
volume_rate: Result<ParticipationRate, CapacityError>,
engine: Engine, engine: Engine,
rebalance_day_counter: usize, rebalance_day_counter: usize,
last_rebalance_date: Option<NaiveDate>, last_rebalance_date: Option<NaiveDate>,
@@ -1776,6 +1778,7 @@ impl PlatformExprStrategy {
.clone() .clone()
.map(PlatformPortfolioDrawdownController::new); .map(PlatformPortfolioDrawdownController::new);
Self { Self {
volume_rate: ParticipationRate::new(config.risk_config.trading_constraints.volume_percent),
config, config,
engine, engine,
protection_fill_count: 0, protection_fill_count: 0,
@@ -3156,9 +3159,9 @@ impl PlatformExprStrategy {
allow_odd_lot_sell: bool, allow_odd_lot_sell: bool,
current_fill_quantity: u32, current_fill_quantity: u32,
execution_state: &ProjectedExecutionState, execution_state: &ProjectedExecutionState,
) -> Option<u32> { ) -> Result<Option<u32>, BacktestError> {
if requested_qty == 0 { if requested_qty == 0 {
return Some(0); return Ok(Some(0));
} }
let constraints = self.config.risk_config.trading_constraints; let constraints = self.config.risk_config.trading_constraints;
@@ -3183,7 +3186,7 @@ impl PlatformExprStrategy {
}; };
if top_level_liquidity == 0 { if top_level_liquidity == 0 {
if quote.is_some() { if quote.is_some() {
return None; return Ok(None);
} }
} else { } else {
let liquidity_limited = if side == OrderSide::Sell && allow_odd_lot_sell { let liquidity_limited = if side == OrderSide::Sell && allow_odd_lot_sell {
@@ -3196,7 +3199,7 @@ impl PlatformExprStrategy {
) )
}; };
if liquidity_limited == 0 { if liquidity_limited == 0 {
return None; return Ok(None);
} }
max_fill = max_fill.min(liquidity_limited); max_fill = max_fill.min(liquidity_limited);
} }
@@ -3209,7 +3212,7 @@ impl PlatformExprStrategy {
None => market.volume, None => market.volume,
}; };
if volume_basis == 0 { if volume_basis == 0 {
return None; return Ok(None);
} }
let consumed_turnover = execution_state let consumed_turnover = execution_state
.intraday_turnover .intraday_turnover
@@ -3217,23 +3220,23 @@ impl PlatformExprStrategy {
.copied() .copied()
.unwrap_or(0) .unwrap_or(0)
.saturating_add(current_fill_quantity); .saturating_add(current_fill_quantity);
let raw_limit = ((volume_basis as f64) * constraints.volume_percent).floor() as i64 let raw_limit = self.volume_rate.map_err(|error| BacktestError::Execution(error.to_string()))?
- consumed_turnover as i64; .remaining(volume_basis, u64::from(consumed_turnover), requested_qty);
if raw_limit <= 0 { if raw_limit == 0 {
return None; return Ok(None);
} }
let volume_limited = if side == OrderSide::Sell && allow_odd_lot_sell { let volume_limited = if side == OrderSide::Sell && allow_odd_lot_sell {
raw_limit as u32 raw_limit
} else { } else {
self.round_lot_quantity(raw_limit as u32, minimum_order_quantity, order_step_size) self.round_lot_quantity(raw_limit, minimum_order_quantity, order_step_size)
}; };
if volume_limited == 0 { if volume_limited == 0 {
return None; return Ok(None);
} }
max_fill = max_fill.min(volume_limited); max_fill = max_fill.min(volume_limited);
} }
Some(max_fill) Ok(Some(max_fill))
} }
fn quote_lacks_level1_depth(quote: &crate::data::IntradayExecutionQuote) -> bool { fn quote_lacks_level1_depth(quote: &crate::data::IntradayExecutionQuote) -> bool {
@@ -3333,7 +3336,7 @@ impl PlatformExprStrategy {
allow_odd_lot_sell, allow_odd_lot_sell,
filled_qty, filled_qty,
execution_state, execution_state,
) )?
.unwrap_or(0); .unwrap_or(0);
if available_qty == 0 { if available_qty == 0 {
break; break;
@@ -3483,7 +3486,7 @@ impl PlatformExprStrategy {
let round_lot = self.projected_round_lot(ctx, symbol); let round_lot = self.projected_round_lot(ctx, symbol);
let minimum_order_quantity = self.projected_minimum_order_quantity(ctx, symbol); let minimum_order_quantity = self.projected_minimum_order_quantity(ctx, symbol);
let order_step_size = self.projected_order_step_size(ctx, symbol); let order_step_size = self.projected_order_step_size(ctx, symbol);
let Some(fill) = self let mut fill = self
.projected_select_execution_fill_at_time( .projected_select_execution_fill_at_time(
ctx, ctx,
date, date,
@@ -3498,22 +3501,18 @@ impl PlatformExprStrategy {
None, None,
execution_state, execution_state,
execution_time, execution_time,
)? )?;
.or_else(|| { if fill.is_none()
if self.uses_intraday_execution_quotes() && (!self.uses_intraday_execution_quotes() || Self::defer_projection_execution_risk(ctx, date))
&& !Self::defer_projection_execution_risk(ctx, date) && !self.has_execution_quote_at_or_before_at_time(
{
return None;
}
if !self.has_execution_quote_at_or_before_at_time(
ctx, ctx,
date, date,
symbol, symbol,
execution_state, execution_state,
execution_time, execution_time,
) && ctx.data.execution_quotes_on(date, symbol).is_empty() ) && ctx.data.execution_quotes_on(date, symbol).is_empty()
{ {
let fallback_quantity = self.projected_market_fillable_quantity( if let Some(fallback_quantity) = self.projected_market_fillable_quantity(
market, market,
None, None,
symbol, symbol,
@@ -3525,21 +3524,18 @@ impl PlatformExprStrategy {
sellable_qty >= current_qty, sellable_qty >= current_qty,
0, 0,
execution_state, execution_state,
)?; )?.filter(|quantity| *quantity > 0)
if fallback_quantity == 0 { {
return None; fill = Some(ProjectedExecutionFill {
}
Some(ProjectedExecutionFill {
price: self.projected_execution_price(market, OrderSide::Sell), price: self.projected_execution_price(market, OrderSide::Sell),
quantity: fallback_quantity, quantity: fallback_quantity,
next_cursor: date.and_time( next_cursor: date.and_time(
execution_time.unwrap_or_else(|| self.intraday_execution_start_time()), execution_time.unwrap_or_else(|| self.intraday_execution_start_time()),
) + Duration::seconds(1), ) + Duration::seconds(1),
}) });
} else { }
None }
} let Some(fill) = fill else { return Ok(None); };
}) else { return Ok(None); };
let gross_amount = fill.price * fill.quantity as f64; let gross_amount = fill.price * fill.quantity as f64;
let net_cash = self.sell_net_cash(date, gross_amount); let net_cash = self.sell_net_cash(date, gross_amount);
projected projected
@@ -4120,7 +4116,7 @@ impl PlatformExprStrategy {
} }
let submitted_quantity = quantity; let submitted_quantity = quantity;
let defer_projection_execution_risk = Self::defer_projection_execution_risk(ctx, date); let defer_projection_execution_risk = Self::defer_projection_execution_risk(ctx, date);
let fill = self let mut fill = self
.projected_select_execution_fill( .projected_select_execution_fill(
ctx, ctx,
date, date,
@@ -4134,23 +4130,22 @@ impl PlatformExprStrategy {
Some(cash_limit), Some(cash_limit),
gross_limit, gross_limit,
execution_state, execution_state,
)? )?;
.or_else(|| { if fill.is_none()
if !defer_projection_execution_risk && !(!defer_projection_execution_risk
&& ctx.data.has_execution_quotes_on_date(date) && ctx.data.has_execution_quotes_on_date(date)
&& ctx.data.execution_quotes_on(date, symbol).is_empty() && ctx.data.execution_quotes_on(date, symbol).is_empty())
{ && !self.has_execution_quote_at_or_before_at_time(
None
} else if !self.has_execution_quote_at_or_before_at_time(
ctx, ctx,
date, date,
symbol, symbol,
execution_state, execution_state,
None, None,
) && ctx.data.execution_quotes_on(date, symbol).is_empty() ) && ctx.data.execution_quotes_on(date, symbol).is_empty()
{ && let Some(market) = ctx.data.market(date, symbol)
let fallback_quantity = self.projected_market_fillable_quantity( {
ctx.data.market(date, symbol)?, if let Some(fallback_quantity) = self.projected_market_fillable_quantity(
market,
None, None,
symbol, symbol,
OrderSide::Buy, OrderSide::Buy,
@@ -4161,20 +4156,16 @@ impl PlatformExprStrategy {
false, false,
0, 0,
execution_state, execution_state,
)?; )?.filter(|quantity| *quantity > 0)
if fallback_quantity == 0 { {
return None; fill = Some(ProjectedExecutionFill {
}
Some(ProjectedExecutionFill {
price: sizing_price, price: sizing_price,
quantity: fallback_quantity, quantity: fallback_quantity,
next_cursor: date.and_time(self.intraday_execution_start_time()) next_cursor: date.and_time(self.intraday_execution_start_time())
+ Duration::seconds(1), + Duration::seconds(1),
}) });
} else { }
None }
}
});
let Some(fill) = fill else { let Some(fill) = fill else {
return Ok(ProjectedOrderValueResult::submitted_without_fill(submitted_quantity)); return Ok(ProjectedOrderValueResult::submitted_without_fill(submitted_quantity));
}; };
@@ -17958,7 +17949,7 @@ mod tests {
false, false,
0, 0,
&execution_state, &execution_state,
), ).expect("valid volume capacity"),
Some(2_500) Some(2_500)
); );
@@ -17978,7 +17969,7 @@ mod tests {
false, false,
0, 0,
&execution_state, &execution_state,
), ).expect("valid remaining volume capacity"),
Some(100) Some(100)
); );
} }