62 lines
2.3 KiB
Rust
62 lines
2.3 KiB
Rust
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<NaiveDateTime>,
|
|
end: Option<NaiveDateTime>,
|
|
quantity: u32,
|
|
) -> Result<Self, BacktestError> {
|
|
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);
|
|
}
|
|
}
|