mantis_ta/indicators/momentum/
roc.rs1use crate::indicators::Indicator;
2use crate::types::Candle;
3use crate::utils::ringbuf::RingBuf;
4
5#[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 assert!(outputs[1].is_some());
115 assert!((outputs[1].unwrap() - 2.0).abs() < 0.0001);
116 assert!(outputs[2].is_some());
118 assert!((outputs[2].unwrap() - 1.961).abs() < 0.01);
119 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 assert!(outputs[1].is_some());
163 assert!((outputs[1].unwrap() - (-5.0)).abs() < 0.0001);
164 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}