fix: size TWAP slices from frozen clocks instead of future quotes
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user