quantwave_core/indicators/
donchian.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5#[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 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 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 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 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 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); 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};