Skip to main content

mantis_ta/indicators/momentum/
roc.rs

1use crate::indicators::Indicator;
2use crate::types::Candle;
3use crate::utils::ringbuf::RingBuf;
4
5/// Rate of Change over closing prices.
6///
7/// Measures the percentage change in price over a specified period.
8/// ROC = ((Close - Close[n periods ago]) / Close[n periods ago]) * 100
9///
10/// # Examples
11/// ```rust
12/// use mantis_ta::indicators::{Indicator, ROC};
13/// use mantis_ta::types::Candle;
14///
15/// let candles: Vec<Candle> = [100.0, 102.0, 104.0, 106.0]
16///     .iter()
17///     .enumerate()
18///     .map(|(i, c)| Candle {
19///         timestamp: i as i64,
20///         open: *c,
21///         high: *c,
22///         low: *c,
23///         close: *c,
24///         volume: 0.0,
25///     })
26///     .collect();
27///
28/// let values = ROC::new(2).calculate(&candles);
29/// assert_eq!(values[0], None);
30/// assert!(values[1].is_some());
31/// assert!(values[2].is_some());
32/// assert!(values[3].is_some());
33/// ```
34#[derive(Debug, Clone)]
35pub struct ROC {
36    period: usize,
37    window: RingBuf<f64>,
38}
39
40impl ROC {
41    pub fn new(period: usize) -> Self {
42        assert!(period > 0, "period must be > 0");
43        Self {
44            period,
45            window: RingBuf::new(period, 0.0),
46        }
47    }
48
49    #[inline]
50    fn update(&mut self, value: f64) -> Option<f64> {
51        self.window.push(value);
52
53        if self.window.len() < self.period {
54            return None;
55        }
56
57        let current = value;
58        let past = self.window.iter().next().copied().unwrap_or(0.0);
59
60        if past.abs() > 1e-10 {
61            Some(((current - past) / past) * 100.0)
62        } else {
63            None
64        }
65    }
66}
67
68impl Indicator for ROC {
69    type Output = f64;
70
71    fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
72        self.update(candle.close)
73    }
74
75    fn reset(&mut self) {
76        self.window = RingBuf::new(self.period, 0.0);
77    }
78
79    fn warmup_period(&self) -> usize {
80        self.period
81    }
82
83    fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
84        Box::new(self.clone())
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn computes_roc_after_warmup() {
94        let mut roc = ROC::new(2);
95        let candles = [100.0, 102.0, 104.0, 106.0]
96            .iter()
97            .map(|c| Candle {
98                timestamp: 0,
99                open: *c,
100                high: *c,
101                low: *c,
102                close: *c,
103                volume: 0.0,
104            })
105            .collect::<Vec<_>>();
106
107        let mut outputs = Vec::new();
108        for c in &candles {
109            outputs.push(roc.next(c));
110        }
111
112        assert_eq!(outputs[0], None);
113        // ROC(2) at 102: ((102 - 100) / 100) * 100 = 2.0
114        assert!(outputs[1].is_some());
115        assert!((outputs[1].unwrap() - 2.0).abs() < 0.0001);
116        // ROC(2) at 104: ((104 - 102) / 102) * 100 ≈ 1.961
117        assert!(outputs[2].is_some());
118        assert!((outputs[2].unwrap() - 1.961).abs() < 0.01);
119        // ROC(2) at 106: ((106 - 104) / 104) * 100 ≈ 1.923
120        assert!(outputs[3].is_some());
121        assert!((outputs[3].unwrap() - 1.923).abs() < 0.01);
122    }
123
124    #[test]
125    fn roc_reset_clears_state() {
126        let mut roc = ROC::new(2);
127        let candle = Candle {
128            timestamp: 0,
129            open: 100.0,
130            high: 100.0,
131            low: 100.0,
132            close: 100.0,
133            volume: 0.0,
134        };
135
136        roc.next(&candle);
137        roc.next(&candle);
138        assert!(roc.next(&candle).is_some());
139
140        roc.reset();
141        assert_eq!(roc.next(&candle), None);
142    }
143
144    #[test]
145    fn roc_with_negative_change() {
146        let mut roc = ROC::new(2);
147        let candles = [100.0, 95.0, 90.0]
148            .iter()
149            .map(|c| Candle {
150                timestamp: 0,
151                open: *c,
152                high: *c,
153                low: *c,
154                close: *c,
155                volume: 0.0,
156            })
157            .collect::<Vec<_>>();
158
159        let outputs: Vec<_> = candles.iter().map(|c| roc.next(c)).collect();
160        assert_eq!(outputs[0], None);
161        // ROC(2) at 95: ((95 - 100) / 100) * 100 = -5.0
162        assert!(outputs[1].is_some());
163        assert!((outputs[1].unwrap() - (-5.0)).abs() < 0.0001);
164        // ROC(2) at 90: ((90 - 95) / 95) * 100 ≈ -5.263
165        assert!(outputs[2].is_some());
166        assert!((outputs[2].unwrap() - (-5.263)).abs() < 0.01);
167    }
168
169    #[test]
170    fn roc_warmup_period() {
171        let roc = ROC::new(5);
172        assert_eq!(roc.warmup_period(), 5);
173    }
174}