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 params: &[ParamDef {
166 name: "period",
167 default: "20",
168 description: "Channel period",
169 }],
170 formula_source: "https://www.investopedia.com/terms/d/donchianchannels.asp",
171 formula_latex: r#"
172\[
173UC = \max(H_{t-n \dots t}) \\ LC = \min(L_{t-n \dots t})
174\]
175"#,
176 gold_standard_file: "donchian.json",
177 category: "Classic",
178};