wickra_core/indicators/
donchian.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct DonchianOutput {
12 pub upper: f64,
14 pub middle: f64,
16 pub lower: f64,
18}
19
20#[derive(Debug, Clone)]
38pub struct Donchian {
39 period: usize,
40 candles: VecDeque<Candle>,
41}
42
43impl Donchian {
44 pub fn new(period: usize) -> Result<Self> {
47 if period == 0 {
48 return Err(Error::PeriodZero);
49 }
50 Ok(Self {
51 period,
52 candles: VecDeque::with_capacity(period),
53 })
54 }
55
56 pub const fn period(&self) -> usize {
58 self.period
59 }
60}
61
62impl Indicator for Donchian {
63 type Input = Candle;
64 type Output = DonchianOutput;
65
66 fn update(&mut self, candle: Candle) -> Option<DonchianOutput> {
67 if self.candles.len() == self.period {
68 self.candles.pop_front();
69 }
70 self.candles.push_back(candle);
71 if self.candles.len() < self.period {
72 return None;
73 }
74 let upper = self
75 .candles
76 .iter()
77 .map(|c| c.high)
78 .fold(f64::NEG_INFINITY, f64::max);
79 let lower = self
80 .candles
81 .iter()
82 .map(|c| c.low)
83 .fold(f64::INFINITY, f64::min);
84 Some(DonchianOutput {
85 upper,
86 middle: f64::midpoint(upper, lower),
87 lower,
88 })
89 }
90
91 fn reset(&mut self) {
92 self.candles.clear();
93 }
94
95 fn warmup_period(&self) -> usize {
96 self.period
97 }
98
99 fn is_ready(&self) -> bool {
100 self.candles.len() == self.period
101 }
102
103 fn name(&self) -> &'static str {
104 "DonchianChannels"
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::traits::BatchExt;
112 use approx::assert_relative_eq;
113
114 fn c(h: f64, l: f64, cl: f64) -> Candle {
115 Candle::new(cl, h, l, cl, 1.0, 0).unwrap()
116 }
117
118 #[test]
119 fn flat_market_yields_equal_bands() {
120 let candles: Vec<Candle> = (0..20).map(|_| c(11.0, 9.0, 10.0)).collect();
121 let mut d = Donchian::new(5).unwrap();
122 let last = d.batch(&candles).into_iter().flatten().last().unwrap();
123 assert_relative_eq!(last.upper, 11.0, epsilon = 1e-12);
124 assert_relative_eq!(last.lower, 9.0, epsilon = 1e-12);
125 assert_relative_eq!(last.middle, 10.0, epsilon = 1e-12);
126 }
127
128 #[test]
129 fn batch_equals_streaming() {
130 let candles: Vec<Candle> = (0..40)
131 .map(|i| c(f64::from(i) + 2.0, f64::from(i), f64::from(i) + 1.0))
132 .collect();
133 let mut a = Donchian::new(10).unwrap();
134 let mut b = Donchian::new(10).unwrap();
135 assert_eq!(
136 a.batch(&candles),
137 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
138 );
139 }
140
141 #[test]
142 fn upper_above_middle_above_lower() {
143 let candles: Vec<Candle> = (0..50)
144 .map(|i| c(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i)))
145 .collect();
146 let mut d = Donchian::new(10).unwrap();
147 for o in d.batch(&candles).into_iter().flatten() {
148 assert!(o.upper >= o.middle);
149 assert!(o.middle >= o.lower);
150 }
151 }
152
153 #[test]
154 fn rejects_zero_period() {
155 assert!(Donchian::new(0).is_err());
156 }
157
158 #[test]
162 fn accessors_and_metadata() {
163 let d = Donchian::new(20).unwrap();
164 assert_eq!(d.period(), 20);
165 assert_eq!(d.warmup_period(), 20);
166 assert_eq!(d.name(), "DonchianChannels");
167 }
168
169 #[test]
170 fn reset_clears_state() {
171 let candles: Vec<Candle> = (0..20)
172 .map(|i| c(f64::from(i) + 1.0, f64::from(i) - 1.0, f64::from(i)))
173 .collect();
174 let mut d = Donchian::new(5).unwrap();
175 d.batch(&candles);
176 assert!(d.is_ready());
177 d.reset();
178 assert!(!d.is_ready());
179 assert_eq!(d.update(candles[0]), None);
180 }
181}