Skip to main content

quantedge_ta/indicators/
dc.rs

1use std::{fmt::Display, num::NonZero};
2
3use crate::{
4    Indicator, IndicatorConfig, IndicatorConfigBuilder, Price, Timestamp,
5    internals::RollingExtremes,
6};
7
8/// Configuration for the Donchian Channel ([`Dc`]) indicator.
9///
10/// The Donchian Channel tracks the highest high and lowest low over a
11/// rolling window of `length` bars. The source is always the OHLC
12/// high/low extremes — the `source` field is fixed to
13/// [`PriceSource::Close`] and has no effect on computation.
14///
15/// # Example
16///
17/// ```
18/// use quantedge_ta::DcConfig;
19///
20/// let config = DcConfig::builder().build();
21/// assert_eq!(config.convergence(), 20);
22/// ```
23#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
24pub struct DcConfig {
25    length: usize,
26}
27
28impl IndicatorConfig for DcConfig {
29    type Builder = DcConfigBuilder;
30
31    #[inline]
32    fn builder() -> Self::Builder {
33        DcConfigBuilder::new()
34    }
35
36    #[inline]
37    fn source(&self) -> crate::PriceSource {
38        crate::PriceSource::Close
39    }
40
41    #[inline]
42    fn convergence(&self) -> usize {
43        self.length
44    }
45}
46
47impl DcConfig {
48    #[must_use]
49    #[inline]
50    pub fn length(&self) -> usize {
51        self.length
52    }
53}
54
55impl Display for DcConfig {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        write!(f, "DcConfig(l: {})", self.length)
58    }
59}
60
61/// Builder for [`DcConfig`].
62///
63/// Defaults: length = 20.
64pub struct DcConfigBuilder {
65    length: usize,
66}
67
68impl DcConfigBuilder {
69    fn new() -> Self {
70        DcConfigBuilder { length: 20 }
71    }
72
73    #[inline]
74    #[must_use]
75    pub fn length(mut self, length: NonZero<usize>) -> Self {
76        self.length = length.get();
77        self
78    }
79}
80
81impl IndicatorConfigBuilder<DcConfig> for DcConfigBuilder {
82    fn source(self, _source: crate::PriceSource) -> Self {
83        self
84    }
85
86    fn build(self) -> DcConfig {
87        DcConfig {
88            length: self.length,
89        }
90    }
91}
92
93/// Donchian Channel output: upper, middle, and lower bands.
94///
95/// ```text
96/// upper  = highest high over the lookback window
97/// lower  = lowest low over the lookback window
98/// middle = (upper + lower) / 2
99/// ```
100#[derive(Debug, Clone, Copy, PartialEq)]
101pub struct DcValue {
102    upper: Price,
103    middle: Price,
104    lower: Price,
105}
106
107impl DcValue {
108    /// Upper band (highest high).
109    #[inline]
110    #[must_use]
111    pub fn upper(&self) -> Price {
112        self.upper
113    }
114
115    /// Middle band: `(upper + lower) / 2`.
116    #[inline]
117    #[must_use]
118    pub fn middle(&self) -> Price {
119        self.middle
120    }
121
122    /// Lower band (lowest low).
123    #[inline]
124    #[must_use]
125    pub fn lower(&self) -> Price {
126        self.lower
127    }
128}
129
130impl Display for DcValue {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        write!(
133            f,
134            "DcValue(u: {}, m: {}, l: {})",
135            self.upper, self.middle, self.lower
136        )
137    }
138}
139
140/// Donchian Channel (DC).
141///
142/// Tracks the highest high and lowest low over a rolling lookback
143/// window, forming an upper and lower channel. The middle line is the
144/// average of the two extremes.
145///
146/// ```text
147/// upper  = max(high, length bars)
148/// lower  = min(low, length bars)
149/// middle = (upper + lower) / 2
150/// ```
151///
152/// Returns `None` until the lookback window is full (`length` bars).
153///
154/// Supports live repainting: feeding a bar with the same `open_time`
155/// recomputes from the previous state without advancing the window.
156///
157/// # Example
158///
159/// ```
160/// use quantedge_ta::{Dc, DcConfig};
161/// # use quantedge_ta::{Ohlcv, Price, Timestamp};
162/// #
163/// # struct Bar { o: f64, h: f64, l: f64, c: f64, t: u64 }
164/// # impl Ohlcv for Bar {
165/// #     fn open(&self) -> Price { self.o }
166/// #     fn high(&self) -> Price { self.h }
167/// #     fn low(&self) -> Price { self.l }
168/// #     fn close(&self) -> Price { self.c }
169/// #     fn open_time(&self) -> Timestamp { self.t }
170/// # }
171///
172/// let mut dc = Dc::new(DcConfig::builder().build());
173/// // Returns None until the lookback window (default 20) is full.
174/// assert!(dc.compute(&Bar { o: 10.0, h: 12.0, l: 8.0, c: 11.0, t: 1 }).is_none());
175/// ```
176#[derive(Clone, Debug)]
177pub struct Dc {
178    config: DcConfig,
179    extremes: RollingExtremes,
180    current: Option<DcValue>,
181    last_open_time: Option<Timestamp>,
182}
183
184impl Indicator for Dc {
185    type Config = DcConfig;
186    type Output = DcValue;
187
188    fn new(config: Self::Config) -> Self {
189        Dc {
190            config,
191            extremes: RollingExtremes::new(config.length),
192            current: None,
193            last_open_time: None,
194        }
195    }
196
197    #[inline]
198    fn compute(&mut self, ohlcv: &impl crate::Ohlcv) -> Option<Self::Output> {
199        debug_assert!(
200            self.last_open_time.is_none_or(|t| t <= ohlcv.open_time()),
201            "open_time must be non-decreasing: last={}, got={}",
202            self.last_open_time.unwrap_or(0),
203            ohlcv.open_time(),
204        );
205
206        let is_next_bar = self.last_open_time.is_none_or(|t| t < ohlcv.open_time());
207
208        let (highest_high, lowest_low) = if is_next_bar {
209            self.last_open_time = Some(ohlcv.open_time());
210            self.extremes.push(ohlcv)
211        } else {
212            self.extremes.replace(ohlcv)
213        };
214
215        self.current = if self.extremes.is_ready() {
216            Some(DcValue {
217                upper: highest_high,
218                middle: (highest_high + lowest_low) * 0.5,
219                lower: lowest_low,
220            })
221        } else {
222            None
223        };
224
225        self.current
226    }
227
228    #[inline]
229    fn value(&self) -> Option<Self::Output> {
230        self.current
231    }
232}
233
234impl Display for Dc {
235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236        write!(f, "Dc(l: {})", self.config.length)
237    }
238}
239
240#[cfg(test)]
241#[allow(clippy::float_cmp)]
242mod tests {
243    use super::*;
244    use crate::test_util::{nz, ohlc};
245
246    fn dc(length: usize) -> Dc {
247        Dc::new(DcConfig::builder().length(nz(length)).build())
248    }
249
250    /// Returns a converged Dc(3) after 3 bars.
251    /// Bars: h/l = (12,8), (14,9), (13,10) at times 1–3.
252    /// Upper=14, lower=8, middle=11.
253    fn seeded_dc() -> Dc {
254        let mut d = dc(3);
255        d.compute(&ohlc(10.0, 12.0, 8.0, 11.0, 1));
256        d.compute(&ohlc(11.0, 14.0, 9.0, 13.0, 2));
257        d.compute(&ohlc(13.0, 13.0, 10.0, 10.0, 3));
258        d
259    }
260
261    mod convergence {
262        use super::*;
263
264        #[test]
265        fn returns_none_during_filling() {
266            let mut d = dc(3);
267            assert!(d.compute(&ohlc(10.0, 12.0, 8.0, 11.0, 1)).is_none());
268            assert!(d.compute(&ohlc(11.0, 14.0, 9.0, 13.0, 2)).is_none());
269        }
270
271        #[test]
272        fn first_value_at_length_bars() {
273            let mut d = dc(3);
274            d.compute(&ohlc(10.0, 12.0, 8.0, 11.0, 1));
275            d.compute(&ohlc(11.0, 14.0, 9.0, 13.0, 2));
276            let val = d.compute(&ohlc(13.0, 13.0, 10.0, 10.0, 3));
277            assert!(val.is_some());
278        }
279
280        #[test]
281        fn length_one_converges_immediately() {
282            let mut d = dc(1);
283            let val = d.compute(&ohlc(10.0, 15.0, 5.0, 12.0, 1));
284            assert!(val.is_some());
285        }
286    }
287
288    mod computation {
289        use super::*;
290
291        #[test]
292        fn upper_lower_middle_math() {
293            // Window bars 1–3: highest_high=14, lowest_low=8
294            let d = seeded_dc();
295            let val = d.value().unwrap();
296            assert_eq!(val.upper(), 14.0);
297            assert_eq!(val.lower(), 8.0);
298            assert_eq!(val.middle(), 11.0);
299        }
300
301        #[test]
302        fn sliding_window_drops_oldest() {
303            let mut d = seeded_dc();
304            // Bar 4: h=15, l=11. Window is now bars 2–4.
305            // highest_high = max(14, 13, 15) = 15
306            // lowest_low = min(9, 10, 11) = 9
307            let val = d.compute(&ohlc(12.0, 15.0, 11.0, 12.0, 4)).unwrap();
308            assert_eq!(val.upper(), 15.0);
309            assert_eq!(val.lower(), 9.0);
310            assert_eq!(val.middle(), 12.0);
311        }
312
313        #[test]
314        fn slides_across_many_bars() {
315            let mut d = dc(2);
316            d.compute(&ohlc(10.0, 20.0, 5.0, 15.0, 1));
317            d.compute(&ohlc(10.0, 18.0, 8.0, 12.0, 2));
318            d.compute(&ohlc(10.0, 16.0, 10.0, 14.0, 3));
319            // Window bars 3–4: h=16,12 → 16, l=10,7 → 7
320            let val = d.compute(&ohlc(10.0, 12.0, 7.0, 10.0, 4)).unwrap();
321            assert_eq!(val.upper(), 16.0);
322            assert_eq!(val.lower(), 7.0);
323            assert_eq!(val.middle(), 11.5);
324        }
325
326        #[test]
327        fn flat_market() {
328            let mut d = dc(3);
329            for t in 1..=5 {
330                let val = d.compute(&ohlc(10.0, 10.0, 10.0, 10.0, t));
331                if let Some(v) = val {
332                    assert_eq!(v.upper(), 10.0);
333                    assert_eq!(v.lower(), 10.0);
334                    assert_eq!(v.middle(), 10.0);
335                }
336            }
337        }
338    }
339
340    mod repaints {
341        use super::*;
342
343        #[test]
344        fn repaint_updates_value() {
345            let mut d = seeded_dc();
346            let original = d.compute(&ohlc(12.0, 16.0, 11.0, 13.0, 4)).unwrap();
347            // Repaint bar 4 with higher high
348            let repainted = d.compute(&ohlc(12.0, 20.0, 11.0, 13.0, 4)).unwrap();
349            assert!(repainted.upper() > original.upper());
350        }
351
352        #[test]
353        fn multiple_repaints_match_single() {
354            let mut d = seeded_dc();
355            d.compute(&ohlc(12.0, 16.0, 11.0, 13.0, 4));
356            d.compute(&ohlc(12.0, 20.0, 6.0, 15.0, 4)); // repaint 1
357            d.compute(&ohlc(12.0, 14.0, 10.0, 11.0, 4)); // repaint 2
358            let final_val = d.compute(&ohlc(12.0, 15.0, 9.0, 12.0, 4)).unwrap();
359
360            let mut clean = seeded_dc();
361            let expected = clean.compute(&ohlc(12.0, 15.0, 9.0, 12.0, 4)).unwrap();
362
363            assert_eq!(final_val.upper(), expected.upper());
364            assert_eq!(final_val.lower(), expected.lower());
365            assert_eq!(final_val.middle(), expected.middle());
366        }
367
368        #[test]
369        fn repaint_then_advance_uses_repainted() {
370            let mut d = seeded_dc();
371            d.compute(&ohlc(12.0, 16.0, 11.0, 13.0, 4));
372            d.compute(&ohlc(12.0, 16.0, 7.0, 13.0, 4)); // repaint bar 4
373            let after = d.compute(&ohlc(14.0, 17.0, 12.0, 14.0, 5)).unwrap();
374
375            let mut clean = seeded_dc();
376            clean.compute(&ohlc(12.0, 16.0, 7.0, 13.0, 4));
377            let expected = clean.compute(&ohlc(14.0, 17.0, 12.0, 14.0, 5)).unwrap();
378
379            assert_eq!(after.upper(), expected.upper());
380            assert_eq!(after.lower(), expected.lower());
381            assert_eq!(after.middle(), expected.middle());
382        }
383
384        #[test]
385        fn repaint_during_filling_has_no_effect_on_convergence() {
386            let mut d = dc(3);
387            d.compute(&ohlc(10.0, 12.0, 8.0, 11.0, 1));
388            d.compute(&ohlc(11.0, 14.0, 9.0, 13.0, 2));
389            d.compute(&ohlc(11.0, 16.0, 7.0, 15.0, 2)); // repaint bar 2
390            assert!(d.value().is_none()); // still filling
391            let val = d.compute(&ohlc(13.0, 13.0, 10.0, 10.0, 3));
392            assert!(val.is_some()); // now converged
393        }
394    }
395
396    mod clone {
397        use super::*;
398
399        #[test]
400        fn produces_independent_state() {
401            let mut d = seeded_dc();
402            let mut cloned = d.clone();
403
404            let orig = d.compute(&ohlc(12.0, 20.0, 11.0, 16.0, 4)).unwrap();
405            let clone_val = cloned.compute(&ohlc(12.0, 13.0, 7.0, 9.0, 4)).unwrap();
406
407            assert!(
408                (orig.upper() - clone_val.upper()).abs() > 1e-10,
409                "divergent inputs should give different upper"
410            );
411        }
412    }
413
414    mod display {
415        use super::*;
416
417        #[test]
418        fn display_config() {
419            let config = DcConfig::builder().length(nz(20)).build();
420            assert_eq!(config.to_string(), "DcConfig(l: 20)");
421        }
422
423        #[test]
424        fn display_dc() {
425            let d = dc(14);
426            assert_eq!(d.to_string(), "Dc(l: 14)");
427        }
428
429        #[test]
430        fn display_value() {
431            let v = DcValue {
432                upper: 100.0,
433                middle: 75.0,
434                lower: 50.0,
435            };
436            assert_eq!(v.to_string(), "DcValue(u: 100, m: 75, l: 50)");
437        }
438    }
439
440    mod config {
441        use super::*;
442        use std::collections::HashSet;
443
444        #[test]
445        fn default_length_is_20() {
446            let config = DcConfig::builder().build();
447            assert_eq!(config.length(), 20);
448        }
449
450        #[test]
451        fn custom_length() {
452            let config = DcConfig::builder().length(nz(50)).build();
453            assert_eq!(config.length(), 50);
454        }
455
456        #[test]
457        fn convergence_equals_length() {
458            let config = DcConfig::builder().length(nz(14)).build();
459            assert_eq!(config.convergence(), 14);
460
461            let config = DcConfig::builder().build();
462            assert_eq!(config.convergence(), 20);
463        }
464
465        #[test]
466        fn source_is_always_close() {
467            let config = DcConfig::builder().source(crate::PriceSource::HL2).build();
468            assert_eq!(config.source(), crate::PriceSource::Close);
469        }
470
471        #[test]
472        fn eq_and_hash() {
473            let a = DcConfig::builder().length(nz(20)).build();
474            let b = DcConfig::builder().length(nz(20)).build();
475            let c = DcConfig::builder().length(nz(10)).build();
476            assert_eq!(a, b);
477            assert_ne!(a, c);
478
479            let mut set = HashSet::new();
480            set.insert(a);
481            assert!(set.contains(&b));
482            assert!(!set.contains(&c));
483        }
484    }
485
486    mod value_accessor {
487        use super::*;
488
489        #[test]
490        fn none_before_convergence() {
491            let d = dc(3);
492            assert_eq!(d.value(), None);
493        }
494
495        #[test]
496        fn matches_last_compute() {
497            let mut d = seeded_dc();
498            let computed = d.compute(&ohlc(12.0, 16.0, 11.0, 14.0, 4));
499            assert_eq!(d.value(), computed);
500        }
501
502        #[test]
503        fn accessors_return_correct_values() {
504            let d = seeded_dc();
505            let val = d.value().unwrap();
506            assert!(val.upper().is_finite());
507            assert!(val.lower().is_finite());
508            assert!(val.middle().is_finite());
509            assert!(val.upper() >= val.lower());
510            assert!(val.middle() >= val.lower());
511            assert!(val.middle() <= val.upper());
512        }
513    }
514
515    #[cfg(debug_assertions)]
516    mod invariants {
517        use super::*;
518
519        #[test]
520        #[should_panic(expected = "open_time must be non-decreasing")]
521        fn panics_on_decreasing_open_time() {
522            let mut d = dc(3);
523            d.compute(&ohlc(10.0, 12.0, 8.0, 11.0, 2));
524            d.compute(&ohlc(11.0, 14.0, 9.0, 13.0, 1));
525        }
526    }
527}