use crate::domain::{ChainSnapshot, Fill, OrderCommand, SimTime, StepIndex};
use crate::error::BacktestError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SimClock {
step: StepIndex,
ts: SimTime,
}
impl SimClock {
#[must_use]
pub const fn new() -> Self {
Self {
step: StepIndex::new(0),
ts: SimTime::new(i64::MIN),
}
}
#[inline]
#[must_use]
pub const fn step(&self) -> StepIndex {
self.step
}
#[inline]
#[must_use]
pub const fn ts(&self) -> SimTime {
self.ts
}
pub fn advance_to(&mut self, ts: SimTime, step: StepIndex) -> Result<(), BacktestError> {
if ts <= self.ts {
return Err(BacktestError::DataOutOfOrder {
step: step.value(),
ts: ts.value(),
prev: self.ts.value(),
});
}
if step.value() < self.step.value() {
return Err(BacktestError::Data(format!(
"step index regressed: step {} is before the previous step {} at ts {}",
step.value(),
self.step.value(),
ts.value(),
)));
}
self.step = step;
self.ts = ts;
Ok(())
}
}
impl Default for SimClock {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Event {
Snapshot(ChainSnapshot),
Decision(Vec<OrderCommand>),
Fill(Fill),
}
#[cfg(test)]
mod tests {
use super::{Event, SimClock};
use crate::domain::{SimTime, StepIndex};
use crate::error::BacktestError;
#[test]
fn test_clock_step_is_monotonic() {
let mut clock = SimClock::new();
let sequence = [(100i64, 0u32), (200, 1), (900, 2), (901, 3)];
let mut prev_step = clock.step().value();
let mut prev_ts = clock.ts().value();
for (ts, step) in sequence {
let outcome = clock.advance_to(SimTime::new(ts), StepIndex::new(step));
assert!(matches!(outcome, Ok(())));
assert!(clock.step().value() >= prev_step);
assert!(clock.ts().value() > prev_ts);
assert_eq!(clock.step().value(), step);
assert_eq!(clock.ts().value(), ts);
prev_step = clock.step().value();
prev_ts = clock.ts().value();
}
}
#[test]
fn test_clock_rejects_reversed_ts_data_out_of_order() {
let mut clock = SimClock::new();
assert!(matches!(
clock.advance_to(SimTime::new(500), StepIndex::new(0)),
Ok(())
));
let reversed = clock.advance_to(SimTime::new(400), StepIndex::new(1));
assert!(matches!(
reversed,
Err(BacktestError::DataOutOfOrder {
step: 1,
ts: 400,
prev: 500,
})
));
let duplicate = clock.advance_to(SimTime::new(500), StepIndex::new(1));
assert!(matches!(
duplicate,
Err(BacktestError::DataOutOfOrder {
step: 1,
ts: 500,
prev: 500,
})
));
assert_eq!(clock.step().value(), 0);
assert_eq!(clock.ts().value(), 500);
}
#[test]
fn test_clock_rejects_step_regression_in_release() {
let mut clock = SimClock::new();
assert!(matches!(
clock.advance_to(SimTime::new(1_000), StepIndex::new(5)),
Ok(())
));
let regressed = clock.advance_to(SimTime::new(2_000), StepIndex::new(3));
assert!(matches!(regressed, Err(BacktestError::Data(_))));
assert_eq!(clock.step().value(), 5);
assert_eq!(clock.ts().value(), 1_000);
}
#[test]
fn test_clock_advance_strictly_later_ts_succeeds() {
let mut clock = SimClock::new();
assert!(matches!(
clock.advance_to(SimTime::new(1_000), StepIndex::new(0)),
Ok(())
));
let later = clock.advance_to(SimTime::new(5_000_000), StepIndex::new(1));
assert!(matches!(later, Ok(())));
assert_eq!(clock.ts().value(), 5_000_000);
assert_eq!(clock.step().value(), 1);
}
#[test]
fn test_clock_default_matches_new() {
let created = SimClock::new();
let defaulted = SimClock::default();
assert_eq!(created, defaulted);
assert_eq!(created.step().value(), 0);
assert_eq!(created.ts().value(), i64::MIN);
}
#[test]
fn test_event_decision_variant_holds_command_buffer() {
let event = Event::Decision(Vec::new());
assert!(matches!(event, Event::Decision(ref cmds) if cmds.is_empty()));
}
}