use crate::checkpoint::CheckpointStore;
use crate::indicator::{Indicator, IndicatorOutput};
use crate::model::Bar;
#[derive(Debug, Clone, PartialEq)]
pub enum BarLifecycle {
Open(Bar),
Update(Bar),
Confirmed(Bar),
Correction(Bar),
}
impl BarLifecycle {
pub fn bar(&self) -> &Bar {
match self {
BarLifecycle::Open(b)
| BarLifecycle::Update(b)
| BarLifecycle::Confirmed(b)
| BarLifecycle::Correction(b) => b,
}
}
}
pub struct LifecycleRunner<I: Indicator + Clone> {
live: I,
checkpoints: CheckpointStore<I>,
}
impl<I: Indicator + Clone> LifecycleRunner<I> {
pub fn new(indicator: I) -> Self {
let checkpoints = CheckpointStore::new();
Self {
live: indicator,
checkpoints,
}
}
pub fn confirmed_indicator(&self) -> Option<&I> {
self.checkpoints.latest().map(|c| &c.state)
}
pub fn on_event(&mut self, event: BarLifecycle) -> Option<IndicatorOutput> {
self.rewind_to_last_confirmed();
let output = self.live.on_bar(event.bar());
if matches!(
event,
BarLifecycle::Confirmed(_) | BarLifecycle::Correction(_)
) {
self.checkpoints.save(&self.live, event.bar().timestamp);
}
output
}
fn rewind_to_last_confirmed(&mut self) {
if !self.checkpoints.restore_into(&mut self.live) {
self.live.reset();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_repeated_update_is_idempotent() {
#[derive(Clone)]
struct SumEngine {
sum: f64,
}
impl Indicator for SumEngine {
fn name(&self) -> &str {
"sum"
}
fn reset(&mut self) {
self.sum = 0.0;
}
fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
self.sum += bar.close;
Some(IndicatorOutput::new(self.sum))
}
}
let mut runner = LifecycleRunner::new(SumEngine { sum: 0.0 });
let bar = Bar::new(1_000, 100.0, 101.0, 99.0, 100.0, 10.0);
let first_update = runner.on_event(BarLifecycle::Open(bar.clone()));
let second_update = runner.on_event(BarLifecycle::Update(bar.clone()));
assert_eq!(first_update.unwrap().value, second_update.unwrap().value);
let confirmed = runner.on_event(BarLifecycle::Confirmed(bar.clone()));
assert_eq!(confirmed.unwrap().value, 100.0);
assert_eq!(runner.confirmed_indicator().unwrap().sum, 100.0);
let next_bar = Bar::new(1_060, 100.0, 102.0, 100.0, 101.0, 10.0);
let next = runner.on_event(BarLifecycle::Confirmed(next_bar));
assert_eq!(next.unwrap().value, 201.0);
}
#[test]
fn test_correction_replaces_last_confirmed_bar() {
#[derive(Clone)]
struct LastCloseEngine {
last_close: f64,
}
impl Indicator for LastCloseEngine {
fn name(&self) -> &str {
"last_close"
}
fn reset(&mut self) {
self.last_close = 0.0;
}
fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
self.last_close = bar.close;
Some(IndicatorOutput::new(self.last_close))
}
}
let mut runner = LifecycleRunner::new(LastCloseEngine { last_close: 0.0 });
let bar = Bar::new(1_000, 100.0, 101.0, 99.0, 100.0, 10.0);
runner.on_event(BarLifecycle::Confirmed(bar));
let corrected_bar = Bar::new(1_000, 100.0, 101.0, 99.0, 103.5, 10.0);
let corrected = runner.on_event(BarLifecycle::Correction(corrected_bar));
assert_eq!(corrected.unwrap().value, 103.5);
assert_eq!(runner.confirmed_indicator().unwrap().last_close, 103.5);
}
}