use chrono::NaiveDateTime; use crate::engine::BacktestError; /// Cumulative TWAP entitlement depends on the frozen clock, not future quotes. pub(crate) struct TwapSchedule { start: NaiveDateTime, end: NaiveDateTime, quantity: u32, } impl TwapSchedule { pub(crate) fn new( start: Option, end: Option, quantity: u32, ) -> Result { let (Some(start), Some(end)) = (start, end) else { return Err(BacktestError::Execution("TWAP requires an explicit start and end clock".into())); }; if end < start || start.date() != end.date() { return Err(BacktestError::Execution("TWAP requires an ordered same-session clock window".into())); } Ok(Self { start, end, quantity }) } pub(crate) fn due_quantity(&self, at: NaiveDateTime, filled: u32) -> u32 { if at < self.start { return 0; } let entitlement = if at >= self.end { self.quantity } else { let elapsed = (at - self.start).num_microseconds().expect("same-day interval") as u128; let duration = (self.end - self.start).num_microseconds().expect("same-day interval") as u128; (u128::from(self.quantity) * elapsed / duration) as u32 }; entitlement.saturating_sub(filled) } } #[cfg(test)] mod tests { use super::*; use chrono::{Duration, NaiveDate}; #[test] fn clock_entitlements_are_exact_and_allow_backlog_without_future_quote_counts() { let start = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap().and_hms_opt(10, 0, 0).unwrap(); let end = start + Duration::minutes(10); let schedule = TwapSchedule::new(Some(start), Some(end), 1_000).unwrap(); assert_eq!(schedule.due_quantity(start, 0), 0); assert_eq!(schedule.due_quantity(start + Duration::minutes(2), 0), 200); assert_eq!(schedule.due_quantity(start + Duration::minutes(5), 100), 400); assert_eq!(schedule.due_quantity(end, 100), 900); assert_eq!(schedule.due_quantity(end, 1_000), 0); assert!(TwapSchedule::new(Some(start), None, 1_000).is_err()); assert!(TwapSchedule::new(Some(end), Some(start), 1_000).is_err()); assert_eq!(TwapSchedule::new(Some(start), Some(start), 1_000).unwrap().due_quantity(start, 0), 1_000); } }