Skip to main content

bot_engine/testing/
quote_source.rs

1//! Quote source trait: Abstraction for quote data providers.
2//!
3//! Implementations:
4//! - `MockQuoteSource`: Manual injection for tests
5//! - `LiveQuoteSource`: Wraps real exchange (future)
6//! - `CsvQuoteSource`: Replay from CSV (future)
7
8use bot_core::{InstrumentId, Quote};
9use std::collections::HashMap;
10
11/// Trait for providing quote data.
12///
13/// This abstraction allows different quote sources to be plugged in:
14/// - Mock quotes for testing
15/// - Live quotes from real exchange
16/// - Historical quotes from CSV
17#[async_trait::async_trait]
18pub trait QuoteSource: Send + Sync {
19    /// Get current quotes for the given instruments
20    async fn poll_quotes(&self, instruments: &[InstrumentId]) -> Vec<Quote>;
21
22    /// Get the last known quote for an instrument (if cached)
23    fn last_quote(&self, instrument: &InstrumentId) -> Option<Quote>;
24
25    /// Update time (for simulation purposes)
26    fn set_time(&mut self, time_ms: i64);
27
28    /// Get current time
29    fn current_time_ms(&self) -> i64;
30}
31
32/// Mock quote source for testing - allows manual quote injection.
33pub struct MockQuoteSource {
34    quotes: HashMap<InstrumentId, Quote>,
35    time_ms: i64,
36}
37
38impl MockQuoteSource {
39    /// Create an empty mock quote source.
40    pub fn new() -> Self {
41        Self {
42            quotes: HashMap::new(),
43            time_ms: bot_core::now_ms(),
44        }
45    }
46
47    /// Set a quote for an instrument
48    pub fn set_quote(&mut self, quote: Quote) {
49        self.quotes.insert(quote.instrument.clone(), quote);
50    }
51
52    /// Clear all quotes
53    pub fn clear(&mut self) {
54        self.quotes.clear();
55    }
56}
57
58impl Default for MockQuoteSource {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64#[async_trait::async_trait]
65impl QuoteSource for MockQuoteSource {
66    async fn poll_quotes(&self, instruments: &[InstrumentId]) -> Vec<Quote> {
67        instruments
68            .iter()
69            .filter_map(|inst| self.quotes.get(inst).cloned())
70            .collect()
71    }
72
73    fn last_quote(&self, instrument: &InstrumentId) -> Option<Quote> {
74        self.quotes.get(instrument).cloned()
75    }
76
77    fn set_time(&mut self, time_ms: i64) {
78        self.time_ms = time_ms;
79    }
80
81    fn current_time_ms(&self) -> i64 {
82        self.time_ms
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use bot_core::{Price, Qty};
90
91    #[tokio::test]
92    async fn test_mock_quote_source() {
93        let mut source = MockQuoteSource::new();
94
95        let instrument = InstrumentId::new("BTC-PERP");
96        let quote = Quote {
97            instrument: instrument.clone(),
98            bid: Price::new(rust_decimal::Decimal::new(50000, 0)),
99            ask: Price::new(rust_decimal::Decimal::new(50010, 0)),
100            bid_size: Qty::new(rust_decimal::Decimal::new(1, 0)),
101            ask_size: Qty::new(rust_decimal::Decimal::new(1, 0)),
102            ts: 0,
103        };
104
105        source.set_quote(quote.clone());
106
107        let quotes = source.poll_quotes(&[instrument.clone()]).await;
108        assert_eq!(quotes.len(), 1);
109        assert_eq!(quotes[0].bid.0, rust_decimal::Decimal::new(50000, 0));
110    }
111}