//! 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, } impl VolumeCapacityMode { pub fn validate(self, enabled: bool, has_execution_observations: bool) -> Result<(), CapacityError> { if !enabled { return Ok(()); } match self { Self::ExecutionObservation if !has_execution_observations => Err(CapacityError::MissingObservation), Self::CompletedBar => Err(CapacityError::MissingCompletedBar), _ => Ok(()), } } pub fn limits_execution_quantity(self) -> bool { self != Self::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, #[error("completed_bar capacity requires declared bar end and availability; an undated daily total is not a completed observation")] MissingCompletedBar, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CapacityAuditSummary { pub mode: VolumeCapacityMode, pub enabled: bool, pub participation_rate: f64, pub audited_symbol_sessions: usize, pub failed_symbol_sessions: usize, pub audit_passed: Option, pub execution_time_capacity_proven: bool, } impl CapacityAuditSummary { pub fn observe(&mut self, audit: &SessionCapacityAudit) { self.audited_symbol_sessions += 1; self.failed_symbol_sessions += usize::from(!audit.passed); self.audit_passed = Some(self.failed_symbol_sessions == 0); } } /// 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 { 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::().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 { 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 { 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); } }