wickra_core/indicators/
donchian_stop.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 DonchianStopOutput {
12 pub stop_long: f64,
14 pub stop_short: f64,
16}
17
18#[derive(Debug, Clone)]
49pub struct DonchianStop {
50 period: usize,
51 highs: VecDeque<f64>,
52 lows: VecDeque<f64>,
53}
54
55impl DonchianStop {
56 pub fn new(period: usize) -> Result<Self> {
61 if period == 0 {
62 return Err(Error::PeriodZero);
63 }
64 if period > crate::error::MAX_PERIOD {
65 return Err(Error::InvalidPeriod {
66 message: crate::error::PERIOD_ABOVE_MAX,
67 });
68 }
69 Ok(Self {
70 period,
71 highs: VecDeque::with_capacity(period),
72 lows: VecDeque::with_capacity(period),
73 })
74 }
75
76 pub fn classic() -> Self {
78 Self::new(10).expect("classic Donchian Stop period is valid")
79 }
80
81 pub const fn period(&self) -> usize {
83 self.period
84 }
85}
86
87impl Indicator for DonchianStop {
88 type Input = Candle;
89 type Output = DonchianStopOutput;
90
91 #[inline]
92 fn update(&mut self, candle: Candle) -> Option<DonchianStopOutput> {
93 if self.highs.len() == self.period {
94 self.highs.pop_front();
95 self.lows.pop_front();
96 }
97 self.highs.push_back(candle.high);
98 self.lows.push_back(candle.low);
99 if self.highs.len() < self.period {
100 return None;
101 }
102 let stop_short = self.highs.iter().copied().fold(f64::NEG_INFINITY, f64::max);
103 let stop_long = self.lows.iter().copied().fold(f64::INFINITY, f64::min);
104 Some(DonchianStopOutput {
105 stop_long,
106 stop_short,
107 })
108 }
109
110 fn reset(&mut self) {
111 self.highs.clear();
112 self.lows.clear();
113 }
114
115 #[inline]
116 fn warmup_period(&self) -> usize {
117 self.period
118 }
119
120 #[inline]
121 fn is_ready(&self) -> bool {
122 self.highs.len() == self.period
123 }
124
125 #[inline]
126 fn name(&self) -> &'static str {
127 "DonchianStop"
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::traits::BatchExt;
135 use approx::assert_relative_eq;
136
137 fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
138 Candle::new(f64::midpoint(high, low), high, low, close, 1.0, ts).unwrap()
139 }
140
141 #[test]
142 fn rejects_zero_period() {
143 assert!(DonchianStop::new(0).is_err());
144 }
145
146 #[test]
147 fn accessors_and_metadata() {
148 let s = DonchianStop::classic();
149 assert_eq!(s.period(), 10);
150 assert_eq!(s.warmup_period(), 10);
151 assert_eq!(s.name(), "DonchianStop");
152 }
153
154 #[test]
155 fn first_emission_matches_warmup() {
156 let candles: Vec<Candle> = (0..10)
157 .map(|i| {
158 let base = 100.0 + i as f64;
159 c(base + 1.0, base - 1.0, base, i)
160 })
161 .collect();
162 let mut s = DonchianStop::new(5).unwrap();
163 let out = s.batch(&candles);
164 for (i, v) in out.iter().enumerate().take(4) {
165 assert!(v.is_none(), "index {i} must be None during warmup");
166 }
167 assert!(out[4].is_some());
168 }
169
170 #[test]
171 fn reference_values_uptrend_window() {
172 let candles: Vec<Candle> = (0..5)
174 .map(|i| {
175 let base = i as f64 + 0.5;
176 c(base + 0.5, base - 0.5, base, i)
177 })
178 .collect();
179 let mut s = DonchianStop::new(5).unwrap();
180 let out = s.batch(&candles);
181 let v = out[4].expect("ready at index 4");
182 assert_relative_eq!(v.stop_short, 5.0, epsilon = 1e-12);
183 assert_relative_eq!(v.stop_long, 0.0, epsilon = 1e-12);
184 }
185
186 #[test]
187 fn constant_series_holds_both_stops() {
188 let candles: Vec<Candle> = (0..30).map(|i| c(11.0, 9.0, 10.0, i)).collect();
189 let mut s = DonchianStop::new(5).unwrap();
190 for v in s.batch(&candles).into_iter().flatten() {
191 assert_relative_eq!(v.stop_short, 11.0, epsilon = 1e-12);
192 assert_relative_eq!(v.stop_long, 9.0, epsilon = 1e-12);
193 }
194 }
195
196 #[test]
197 fn reset_clears_state() {
198 let candles: Vec<Candle> = (0..30)
199 .map(|i| {
200 let base = 100.0 + i as f64;
201 c(base + 1.0, base - 1.0, base, i)
202 })
203 .collect();
204 let mut s = DonchianStop::classic();
205 s.batch(&candles);
206 assert!(s.is_ready());
207 s.reset();
208 assert!(!s.is_ready());
209 assert_eq!(s.update(candles[0]), None);
210 }
211
212 #[test]
213 fn batch_equals_streaming() {
214 let candles: Vec<Candle> = (0..80)
215 .map(|i| {
216 let mid = 100.0 + (i as f64 * 0.3).sin() * 8.0;
217 c(mid + 1.5, mid - 1.5, mid + 0.5, i)
218 })
219 .collect();
220 let mut a = DonchianStop::classic();
221 let mut b = DonchianStop::classic();
222 assert_eq!(
223 a.batch(&candles),
224 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
225 );
226 }
227}