Skip to main content

quantwave_core/indicators/
donchian.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5/// Donchian Channels
6/// Upper Band: Highest high over the last N periods.
7/// Lower Band: Lowest low over the last N periods.
8/// Middle Band: (Upper + Lower) / 2
9#[derive(Debug, Clone)]
10pub struct DonchianChannels {
11    period: usize,
12    highs: VecDeque<f64>,
13    lows: VecDeque<f64>,
14}
15
16impl DonchianChannels {
17    pub fn new(period: usize) -> Self {
18        Self {
19            period,
20            highs: VecDeque::with_capacity(period),
21            lows: VecDeque::with_capacity(period),
22        }
23    }
24}
25
26impl Next<(f64, f64)> for DonchianChannels {
27    type Output = (f64, f64, f64);
28
29    fn next(&mut self, (high, low): (f64, f64)) -> Self::Output {
30        self.highs.push_back(high);
31        self.lows.push_back(low);
32
33        if self.highs.len() > self.period {
34            self.highs.pop_front();
35            self.lows.pop_front();
36        }
37
38        let mut max_high = f64::MIN;
39        let mut min_low = f64::MAX;
40
41        for &h in self.highs.iter() {
42            if h > max_high {
43                max_high = h;
44            }
45        }
46
47        for &l in self.lows.iter() {
48            if l < min_low {
49                min_low = l;
50            }
51        }
52
53        let middle = (max_high + min_low) / 2.0;
54
55        (max_high, middle, min_low)
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use proptest::prelude::*;
63    use serde::Deserialize;
64    use std::fs;
65    use std::path::Path;
66
67    #[derive(Debug, Deserialize)]
68    struct DonchianCase {
69        highs: Vec<f64>,
70        lows: Vec<f64>,
71        expected_middle: Vec<f64>,
72    }
73
74    #[test]
75    fn test_donchian_gold_standard() {
76        let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
77        let manifest_path = Path::new(&manifest_dir);
78        let path = manifest_path.join("tests/gold_standard/donchian_5.json");
79        let path = if path.exists() {
80            path
81        } else {
82            manifest_path
83                .parent()
84                .unwrap()
85                .join("tests/gold_standard/donchian_5.json")
86        };
87        let content = fs::read_to_string(path).unwrap();
88        let case: DonchianCase = serde_json::from_str(&content).unwrap();
89
90        let mut dc = DonchianChannels::new(5);
91        for i in 0..case.highs.len() {
92            let (_, middle, _) = dc.next((case.highs[i], case.lows[i]));
93            approx::assert_relative_eq!(middle, case.expected_middle[i]);
94        }
95    }
96
97    #[test]
98    fn test_donchian_basic() {
99        let mut dc = DonchianChannels::new(3);
100
101        // bar 1: H=10, L=8 -> U=10, M=9, L=8
102        let (u1, m1, l1) = dc.next((10.0, 8.0));
103        assert_eq!(u1, 10.0);
104        assert_eq!(m1, 9.0);
105        assert_eq!(l1, 8.0);
106
107        // bar 2: H=12, L=7 -> U=12, M=9.5, L=7
108        let (u2, m2, l2) = dc.next((12.0, 7.0));
109        assert_eq!(u2, 12.0);
110        assert_eq!(m2, 9.5);
111        assert_eq!(l2, 7.0);
112
113        // bar 3: H=11, L=9 -> U=12, M=9.5, L=7
114        let (u3, m3, l3) = dc.next((11.0, 9.0));
115        assert_eq!(u3, 12.0);
116        assert_eq!(m3, 9.5);
117        assert_eq!(l3, 7.0);
118
119        // bar 4: H=13, L=10 -> U=13, M=10, L=7 (bar 1 is out)
120        let (u4, m4, l4) = dc.next((13.0, 10.0));
121        assert_eq!(u4, 13.0);
122        assert_eq!(m4, 10.0);
123        assert_eq!(l4, 7.0);
124    }
125
126    fn donchian_batch(data: Vec<(f64, f64)>, period: usize) -> Vec<f64> {
127        let mut dc = DonchianChannels::new(period);
128        // We'll just return the middle band for parity check to simplify,
129        // or we could change the parity helper to handle tuples.
130        data.into_iter().map(|x| dc.next(x).1).collect()
131    }
132
133    proptest! {
134        #[test]
135        fn test_donchian_parity(highs in prop::collection::vec(0.0..1000.0, 1..100), lows in prop::collection::vec(0.0..1000.0, 1..100)) {
136            let len = highs.len().min(lows.len());
137            let highs: Vec<f64> = highs[..len].to_vec();
138            let lows: Vec<f64> = lows[..len].to_vec();
139            let mut input = Vec::with_capacity(len);
140            for i in 0..len {
141                let h = highs[i];
142                let l = lows[i].min(h); // Ensure low <= high
143                input.push((h, l));
144            }
145
146            let period = 5;
147            let mut dc = DonchianChannels::new(period);
148            let mut streaming_results = Vec::with_capacity(len);
149            for &val in &input {
150                streaming_results.push(dc.next(val).1);
151            }
152
153            let batch_results = donchian_batch(input, period);
154
155            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
156                assert_eq!(s, b);
157            }
158        }
159    }
160}
161
162pub const DONCHIAN_METADATA: IndicatorMetadata = IndicatorMetadata {
163    name: "Donchian Channels",
164    description: "Donchian Channels are volatility indicators formed by taking the highest high and the lowest low of the last N periods.",
165    usage: "Use for breakout trading systems: a close above the N-period high signals a long entry; below the N-period low signals a short entry. The Turtle Traders famously used 20 and 55-day Donchian channels.",
166    keywords: &["breakout", "volatility", "trend", "classic", "support-resistance"],
167    ehlers_summary: "Developed by Richard Donchian in the 1970s, Donchian Channels plot the highest high and lowest low over N bars. They define the current trading range and signal breakouts when price escapes the channel. The Turtle Trading system of Richard Dennis built its entire entry and exit logic on 20 and 55-day Donchian channels. — TurtleTrader.com",
168    params: &[ParamDef {
169        name: "period",
170        default: "20",
171        description: "Channel period",
172    }],
173    formula_source: "https://www.investopedia.com/terms/d/donchianchannels.asp",
174    formula_latex: r#"
175\[
176UC = \max(H_{t-n \dots t}) \\ LC = \min(L_{t-n \dots t})
177\]
178"#,
179    gold_standard_file: "donchian.json",
180    category: "Classic",
181};