opendeviationbar-streaming 13.78.0

Real-time streaming engine for open deviation bar processing
Documentation
//! Core traits for the unified engine (Issue #178)
//!
//! Three traits (~50 LOC) that enable swappable source/sink composition.

use async_trait::async_trait;
use std::time::Duration;

use crate::live_engine::CompletedBar;

/// Error type for sink operations, distinguishing recoverable from unrecoverable.
/// Inspired by barter-rs `engine/error.rs`.
#[derive(Debug, thiserror::Error)]
pub enum SinkError {
    /// Transient failure (channel full, network timeout) — log and continue.
    #[error("recoverable: {0}")]
    Recoverable(String),
    /// Terminal failure (serialization error, invalid state) — escalate.
    #[error("unrecoverable: {0}")]
    Unrecoverable(String),
}

/// Checkpoint for cross-session source recovery.
#[derive(Debug, Clone)]
pub struct SourceCheckpoint {
    pub last_ref_id: i64,
    pub timestamp_ms: i64,
}

/// Swappable bar producer (live WS, replay, Vision).
#[async_trait]
pub trait BarSource: Send {
    /// Yield the next completed bar, or `None` on timeout/shutdown.
    async fn next_bar(&mut self, timeout: Duration) -> Option<CompletedBar>;

    /// Snapshot source state for checkpoint recovery.
    fn snapshot(&self) -> Option<SourceCheckpoint>;
}

/// Synchronous bar consumer (channel to Python, SSE, strategy).
pub trait BarSink: Send {
    /// Process a completed bar. Called once per bar in the engine fan-out loop.
    fn on_bar(&mut self, bar: &CompletedBar) -> Result<(), SinkError>;

    /// Flush any buffered state (called on shutdown).
    fn flush(&mut self) -> Result<(), SinkError>;

    /// Human-readable sink name for diagnostics.
    fn name(&self) -> &str;
}

/// Time abstraction (live wall clock or historical event time).
pub trait EngineClock: Send + Sync {
    /// Current time in milliseconds since epoch.
    fn now_ms(&self) -> i64;
}

#[cfg(test)]
mod tests {
    use super::*;

    struct NoopSink;
    impl BarSink for NoopSink {
        fn on_bar(&mut self, _bar: &CompletedBar) -> Result<(), SinkError> {
            Ok(())
        }
        fn flush(&mut self) -> Result<(), SinkError> {
            Ok(())
        }
        fn name(&self) -> &str {
            "noop"
        }
    }

    #[test]
    fn test_sink_error_display() {
        let r = SinkError::Recoverable("channel full".into());
        assert!(r.to_string().contains("recoverable"));
        let u = SinkError::Unrecoverable("serialization failed".into());
        assert!(u.to_string().contains("unrecoverable"));
    }

    #[test]
    fn test_noop_sink() {
        let mut sink = NoopSink;
        assert_eq!(sink.name(), "noop");
        assert!(sink.flush().is_ok());
    }
}