Skip to main content

sandbox_quant/model/
candle.rs

1#[derive(Debug, Clone)]
2pub struct Candle {
3    pub open: f64,
4    pub high: f64,
5    pub low: f64,
6    pub close: f64,
7    pub open_time: u64,
8    pub close_time: u64,
9}
10
11impl Candle {
12    pub fn is_bullish(&self) -> bool {
13        self.close >= self.open
14    }
15}
16
17/// Aggregates real-time trade ticks into a single candle over a time interval.
18#[derive(Debug, Clone)]
19pub struct CandleBuilder {
20    pub open: f64,
21    pub high: f64,
22    pub low: f64,
23    pub close: f64,
24    pub open_time: u64,
25    pub close_time: u64,
26}
27
28impl CandleBuilder {
29    /// Start a new candle. The bucket is aligned to the interval.
30    pub fn new(price: f64, timestamp_ms: u64, interval_ms: u64) -> Self {
31        let open_time = timestamp_ms - (timestamp_ms % interval_ms);
32        Self {
33            open: price,
34            high: price,
35            low: price,
36            close: price,
37            open_time,
38            close_time: open_time + interval_ms,
39        }
40    }
41
42    /// Update the candle with a new trade price.
43    pub fn update(&mut self, price: f64) {
44        self.high = self.high.max(price);
45        self.low = self.low.min(price);
46        self.close = price;
47    }
48
49    /// Check if a timestamp belongs to this candle's time bucket.
50    pub fn contains(&self, timestamp_ms: u64) -> bool {
51        timestamp_ms >= self.open_time && timestamp_ms < self.close_time
52    }
53
54    /// Finalize into an immutable Candle.
55    pub fn finish(&self) -> Candle {
56        Candle {
57            open: self.open,
58            high: self.high,
59            low: self.low,
60            close: self.close,
61            open_time: self.open_time,
62            close_time: self.close_time,
63        }
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn candle_builder_basics() {
73        let mut cb = CandleBuilder::new(100.0, 60_500, 60_000);
74        assert_eq!(cb.open_time, 60_000);
75        assert_eq!(cb.close_time, 120_000);
76        assert!(cb.contains(60_500));
77        assert!(cb.contains(119_999));
78        assert!(!cb.contains(120_000));
79
80        cb.update(105.0);
81        cb.update(95.0);
82        cb.update(102.0);
83
84        let candle = cb.finish();
85        assert!((candle.open - 100.0).abs() < f64::EPSILON);
86        assert!((candle.high - 105.0).abs() < f64::EPSILON);
87        assert!((candle.low - 95.0).abs() < f64::EPSILON);
88        assert!((candle.close - 102.0).abs() < f64::EPSILON);
89        assert!(candle.is_bullish());
90    }
91
92    #[test]
93    fn bearish_candle() {
94        let candle = Candle {
95            open: 100.0,
96            high: 105.0,
97            low: 90.0,
98            close: 95.0,
99            open_time: 0,
100            close_time: 60_000,
101        };
102        assert!(!candle.is_bullish());
103    }
104}