use crate::checkpoint::CheckpointStore;
use crate::indicator::{Indicator, IndicatorOutput};
use crate::model::Bar;
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub enum BarLifecycle {
Open(Bar),
Update(Bar),
Confirmed(Bar),
Correction(Bar),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LifecycleError {
CorrectionTimestampMismatch {
last_confirmed_timestamp: Option<i64>,
attempted_timestamp: i64,
},
NonMonotonicConfirmation {
last_confirmed_timestamp: i64,
attempted_timestamp: i64,
},
}
impl fmt::Display for LifecycleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LifecycleError::CorrectionTimestampMismatch {
last_confirmed_timestamp,
attempted_timestamp,
} => write!(
f,
"correction for timestamp {attempted_timestamp} does not match the last \
confirmed timestamp {last_confirmed_timestamp:?}; only the most recently \
confirmed bar can be corrected"
),
LifecycleError::NonMonotonicConfirmation {
last_confirmed_timestamp,
attempted_timestamp,
} => write!(
f,
"confirmed timestamp {attempted_timestamp} is older than the last confirmed \
timestamp {last_confirmed_timestamp}; only the most recently confirmed bar can \
be replaced"
),
}
}
}
impl std::error::Error for LifecycleError {}
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,
pre_confirmed: CheckpointStore<I>,
post_confirmed: CheckpointStore<I>,
}
impl<I: Indicator + Clone> LifecycleRunner<I> {
pub fn new(indicator: I) -> Self {
Self {
live: indicator,
pre_confirmed: CheckpointStore::new(),
post_confirmed: CheckpointStore::new(),
}
}
pub fn confirmed_indicator(&self) -> Option<&I> {
self.post_confirmed.latest().map(|c| &c.state)
}
pub fn on_event(
&mut self,
event: BarLifecycle,
) -> Result<Option<IndicatorOutput>, LifecycleError> {
match event {
BarLifecycle::Open(bar) | BarLifecycle::Update(bar) => {
self.rewind_to_last_confirmed();
Ok(self.live.on_bar(&bar))
}
BarLifecycle::Confirmed(bar) => match self.last_confirmed_timestamp() {
Some(ts) if bar.timestamp == ts => Ok(self.replace_last_confirmed(&bar)),
Some(ts) if bar.timestamp < ts => Err(LifecycleError::NonMonotonicConfirmation {
last_confirmed_timestamp: ts,
attempted_timestamp: bar.timestamp,
}),
_ => {
self.rewind_to_last_confirmed();
let pre_state = self.live.clone();
let output = self.live.on_bar(&bar);
self.pre_confirmed.save(&pre_state, bar.timestamp);
self.post_confirmed.save(&self.live, bar.timestamp);
Ok(output)
}
},
BarLifecycle::Correction(bar) => match self.last_confirmed_timestamp() {
Some(ts) if bar.timestamp == ts => Ok(self.replace_last_confirmed(&bar)),
last_ts => Err(LifecycleError::CorrectionTimestampMismatch {
last_confirmed_timestamp: last_ts,
attempted_timestamp: bar.timestamp,
}),
},
}
}
fn replace_last_confirmed(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
if !self.pre_confirmed.restore_into(&mut self.live) {
self.live.reset();
}
let output = self.live.on_bar(bar);
self.post_confirmed.save(&self.live, bar.timestamp);
output
}
fn last_confirmed_timestamp(&self) -> Option<i64> {
self.post_confirmed.latest().map(|c| c.timestamp)
}
fn rewind_to_last_confirmed(&mut self) {
if !self.post_confirmed.restore_into(&mut self.live) {
self.live.reset();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::indicator::moving_averages::EmaEngine;
#[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))
}
}
#[test]
fn test_repeated_update_is_idempotent() {
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())).unwrap();
let second_update = runner.on_event(BarLifecycle::Update(bar.clone())).unwrap();
assert_eq!(first_update.unwrap().value, second_update.unwrap().value);
let confirmed = runner
.on_event(BarLifecycle::Confirmed(bar.clone()))
.unwrap();
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)).unwrap();
assert_eq!(next.unwrap().value, 201.0);
}
#[test]
fn test_correction_replaces_rather_than_accumulates() {
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);
runner.on_event(BarLifecycle::Confirmed(bar)).unwrap();
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))
.unwrap();
assert_eq!(corrected.unwrap().value, 103.5);
assert_eq!(runner.confirmed_indicator().unwrap().sum, 103.5);
}
#[test]
fn test_correction_of_second_bar_preserves_first_bar() {
let mut runner = LifecycleRunner::new(SumEngine { sum: 0.0 });
runner
.on_event(BarLifecycle::Confirmed(Bar::new(
1_000, 100.0, 101.0, 99.0, 100.0, 10.0,
)))
.unwrap();
runner
.on_event(BarLifecycle::Confirmed(Bar::new(
1_060, 100.0, 102.0, 100.0, 101.0, 10.0,
)))
.unwrap();
let corrected = runner
.on_event(BarLifecycle::Correction(Bar::new(
1_060, 100.0, 102.0, 100.0, 102.0, 10.0,
)))
.unwrap();
assert_eq!(corrected.unwrap().value, 202.0);
assert_eq!(runner.confirmed_indicator().unwrap().sum, 202.0);
}
#[test]
fn test_correction_with_mismatched_timestamp_is_rejected() {
let mut runner = LifecycleRunner::new(SumEngine { sum: 0.0 });
runner
.on_event(BarLifecycle::Confirmed(Bar::new(
1_000, 100.0, 101.0, 99.0, 100.0, 10.0,
)))
.unwrap();
runner
.on_event(BarLifecycle::Confirmed(Bar::new(
1_060, 100.0, 102.0, 100.0, 101.0, 10.0,
)))
.unwrap();
let err = runner
.on_event(BarLifecycle::Correction(Bar::new(
1_000, 100.0, 101.0, 99.0, 999.0, 10.0,
)))
.unwrap_err();
assert_eq!(
err,
LifecycleError::CorrectionTimestampMismatch {
last_confirmed_timestamp: Some(1_060),
attempted_timestamp: 1_000,
}
);
assert_eq!(runner.confirmed_indicator().unwrap().sum, 201.0);
}
#[test]
fn test_repeated_confirmed_for_same_timestamp_replaces() {
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);
runner.on_event(BarLifecycle::Confirmed(bar)).unwrap();
let re_confirmed = Bar::new(1_000, 100.0, 101.0, 99.0, 105.0, 10.0);
let output = runner
.on_event(BarLifecycle::Confirmed(re_confirmed))
.unwrap();
assert_eq!(output.unwrap().value, 105.0);
assert_eq!(runner.confirmed_indicator().unwrap().sum, 105.0);
}
#[test]
fn test_confirmed_with_older_timestamp_is_rejected() {
let mut runner = LifecycleRunner::new(SumEngine { sum: 0.0 });
runner
.on_event(BarLifecycle::Confirmed(Bar::new(
1_060, 100.0, 102.0, 100.0, 101.0, 10.0,
)))
.unwrap();
let err = runner
.on_event(BarLifecycle::Confirmed(Bar::new(
1_000, 100.0, 101.0, 99.0, 100.0, 10.0,
)))
.unwrap_err();
assert_eq!(
err,
LifecycleError::NonMonotonicConfirmation {
last_confirmed_timestamp: 1_060,
attempted_timestamp: 1_000,
}
);
assert_eq!(runner.confirmed_indicator().unwrap().sum, 101.0);
}
#[test]
fn test_correction_replaces_for_real_indicator() {
let mut runner = LifecycleRunner::new(EmaEngine::new(2));
let k = 2.0 / 3.0;
runner
.on_event(BarLifecycle::Confirmed(Bar::new(
1_000, 100.0, 101.0, 99.0, 100.0, 10.0,
)))
.unwrap();
let confirmed = runner
.on_event(BarLifecycle::Confirmed(Bar::new(
1_060, 100.0, 106.0, 100.0, 106.0, 10.0,
)))
.unwrap();
let ema_after_second = 106.0 * k + 100.0 * (1.0 - k);
assert!((confirmed.unwrap().value - ema_after_second).abs() < 1e-9);
let corrected = runner
.on_event(BarLifecycle::Correction(Bar::new(
1_060, 100.0, 109.0, 100.0, 109.0, 10.0,
)))
.unwrap();
let expected_corrected_ema = 109.0 * k + 100.0 * (1.0 - k);
assert!((corrected.unwrap().value - expected_corrected_ema).abs() < 1e-9);
let buggy_double_applied = 109.0 * k + ema_after_second * (1.0 - k);
assert!((expected_corrected_ema - buggy_double_applied).abs() > 1e-6);
}
}