mantis_ta/indicators/support_resistance/
donchian.rs1use crate::indicators::Indicator;
2use crate::types::{Candle, DonchianOutput};
3use crate::utils::ringbuf::RingBuf;
4
5#[derive(Debug, Clone)]
37pub struct DonchianChannels {
38 period: usize,
39 window: RingBuf<(f64, f64)>,
40}
41
42impl DonchianChannels {
43 pub fn new(period: usize) -> Self {
44 assert!(period > 0, "period must be > 0");
45 Self {
46 period,
47 window: RingBuf::new(period, (0.0, 0.0)),
48 }
49 }
50}
51
52impl Indicator for DonchianChannels {
53 type Output = DonchianOutput;
54
55 fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
56 self.window.push((candle.high, candle.low));
57 if self.window.len() < self.period {
58 return None;
59 }
60
61 let mut upper = f64::MIN;
62 let mut lower = f64::MAX;
63 for &(h, l) in self.window.iter() {
64 if h > upper {
65 upper = h;
66 }
67 if l < lower {
68 lower = l;
69 }
70 }
71
72 Some(DonchianOutput {
73 upper,
74 middle: (upper + lower) / 2.0,
75 lower,
76 })
77 }
78
79 fn reset(&mut self) {
80 self.window = RingBuf::new(self.period, (0.0, 0.0));
81 }
82
83 fn warmup_period(&self) -> usize {
84 self.period
85 }
86
87 fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
88 Box::new(self.clone())
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 fn candle(high: f64, low: f64) -> Candle {
97 Candle {
98 timestamp: 0,
99 open: (high + low) / 2.0,
100 high,
101 low,
102 close: (high + low) / 2.0,
103 volume: 1_000.0,
104 }
105 }
106
107 #[test]
108 fn donchian_emits_after_warmup() {
109 let mut dc = DonchianChannels::new(5);
110 let candles: Vec<Candle> = (0..8)
111 .map(|i| candle(100.0 + i as f64, 99.0 + i as f64))
112 .collect();
113
114 let outputs: Vec<_> = candles.iter().map(|c| dc.next(c)).collect();
115 let wp = dc.warmup_period();
116 assert!(outputs.iter().take(wp - 1).all(|o| o.is_none()));
117 assert!(outputs[wp - 1].is_some());
118 }
119
120 #[test]
121 fn donchian_tracks_highest_high_lowest_low() {
122 let mut dc = DonchianChannels::new(3);
123 let candles: Vec<Candle> = [(10.0, 5.0), (12.0, 6.0), (8.0, 3.0), (9.0, 4.0), (7.0, 2.0)]
124 .iter()
125 .map(|&(h, l)| candle(h, l))
126 .collect();
127
128 let outputs: Vec<_> = candles.iter().map(|c| dc.next(c)).collect();
129
130 let out2 = outputs[2].unwrap();
132 assert!((out2.upper - 12.0).abs() < 1e-9);
133 assert!((out2.lower - 3.0).abs() < 1e-9);
134 assert!((out2.middle - 7.5).abs() < 1e-9);
135
136 let out3 = outputs[3].unwrap();
138 assert!((out3.upper - 12.0).abs() < 1e-9);
139 assert!((out3.lower - 3.0).abs() < 1e-9);
140
141 let out4 = outputs[4].unwrap();
143 assert!((out4.upper - 9.0).abs() < 1e-9);
144 assert!((out4.lower - 2.0).abs() < 1e-9);
145 }
146
147 #[test]
148 fn donchian_bands_bracket_middle() {
149 let mut dc = DonchianChannels::new(4);
150 let candles: Vec<Candle> = (0..8)
151 .map(|i| candle(100.0 + i as f64, 99.0 - i as f64))
152 .collect();
153
154 for c in &candles {
155 if let Some(out) = dc.next(c) {
156 assert!(out.upper >= out.middle);
157 assert!(out.middle >= out.lower);
158 assert!((out.middle - (out.upper + out.lower) / 2.0).abs() < 1e-9);
159 }
160 }
161 }
162
163 #[test]
164 fn donchian_flat_prices_zero_width_band() {
165 let mut dc = DonchianChannels::new(3);
166 let candles: Vec<Candle> = (0..5).map(|_| candle(50.0, 50.0)).collect();
167
168 for c in &candles {
169 if let Some(out) = dc.next(c) {
170 assert!((out.upper - 50.0).abs() < 1e-9);
171 assert!((out.lower - 50.0).abs() < 1e-9);
172 assert!((out.middle - 50.0).abs() < 1e-9);
173 }
174 }
175 }
176
177 #[test]
178 fn donchian_streaming_matches_batch() {
179 let candles: Vec<Candle> = (0..15)
180 .map(|i| candle(90.0 + i as f64 * 0.7, 89.0 + i as f64 * 0.7))
181 .collect();
182
183 let batch = DonchianChannels::new(5).calculate(&candles);
184
185 let mut streamed_dc = DonchianChannels::new(5);
186 let streamed: Vec<_> = candles.iter().map(|c| streamed_dc.next(c)).collect();
187
188 assert_eq!(streamed, batch);
189 }
190
191 #[test]
192 fn donchian_reset_clears_state() {
193 let mut dc = DonchianChannels::new(4);
194 let candles: Vec<Candle> = (0..6)
195 .map(|i| candle(100.0 + i as f64, 99.0 + i as f64))
196 .collect();
197 for c in &candles {
198 dc.next(c);
199 }
200 dc.reset();
201
202 let mut fresh = DonchianChannels::new(4);
203 for c in &candles {
204 assert_eq!(dc.next(c), fresh.next(c));
205 }
206 }
207
208 #[test]
209 #[should_panic(expected = "period must be > 0")]
210 fn donchian_rejects_zero_period() {
211 DonchianChannels::new(0);
212 }
213}