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