1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::atr::Atr;
7use crate::ohlcv::Candle;
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct ChandeKrollStopOutput {
13 pub stop_long: f64,
15 pub stop_short: f64,
17}
18
19#[derive(Debug, Clone)]
54pub struct ChandeKrollStop {
55 atr_period: usize,
56 atr_multiplier: f64,
57 stop_period: usize,
58 atr: Atr,
59 highs: VecDeque<f64>,
60 lows: VecDeque<f64>,
61 high_stops: VecDeque<f64>,
62 low_stops: VecDeque<f64>,
63}
64
65impl ChandeKrollStop {
66 pub fn new(atr_period: usize, atr_multiplier: f64, stop_period: usize) -> Result<Self> {
73 if !atr_multiplier.is_finite() || atr_multiplier <= 0.0 {
74 return Err(Error::NonPositiveMultiplier);
75 }
76 if stop_period == 0 {
77 return Err(Error::PeriodZero);
78 }
79 if stop_period > crate::error::MAX_PERIOD {
80 return Err(Error::InvalidPeriod {
81 message: crate::error::PERIOD_ABOVE_MAX,
82 });
83 }
84 Ok(Self {
85 atr_period,
86 atr_multiplier,
87 stop_period,
88 atr: Atr::new(atr_period)?,
89 highs: VecDeque::with_capacity(atr_period),
90 lows: VecDeque::with_capacity(atr_period),
91 high_stops: VecDeque::with_capacity(stop_period),
92 low_stops: VecDeque::with_capacity(stop_period),
93 })
94 }
95
96 pub fn classic() -> Self {
98 Self::new(10, 1.0, 9).expect("classic Chande Kroll Stop params are valid")
99 }
100
101 pub const fn params(&self) -> (usize, f64, usize) {
103 (self.atr_period, self.atr_multiplier, self.stop_period)
104 }
105}
106
107impl Indicator for ChandeKrollStop {
108 type Input = Candle;
109 type Output = ChandeKrollStopOutput;
110
111 #[inline]
112 fn update(&mut self, candle: Candle) -> Option<ChandeKrollStopOutput> {
113 let atr = self.atr.update(candle);
114 if self.highs.len() == self.atr_period {
115 self.highs.pop_front();
116 self.lows.pop_front();
117 }
118 self.highs.push_back(candle.high);
119 self.lows.push_back(candle.low);
120 if self.highs.len() < self.atr_period {
121 return None;
122 }
123 let atr = atr?;
126 let highest = self.highs.iter().copied().fold(f64::NEG_INFINITY, f64::max);
127 let lowest = self.lows.iter().copied().fold(f64::INFINITY, f64::min);
128 let high_stop = highest - self.atr_multiplier * atr;
129 let low_stop = lowest + self.atr_multiplier * atr;
130
131 if self.high_stops.len() == self.stop_period {
132 self.high_stops.pop_front();
133 self.low_stops.pop_front();
134 }
135 self.high_stops.push_back(high_stop);
136 self.low_stops.push_back(low_stop);
137 if self.high_stops.len() < self.stop_period {
138 return None;
139 }
140 let stop_short = self
141 .high_stops
142 .iter()
143 .copied()
144 .fold(f64::NEG_INFINITY, f64::max);
145 let stop_long = self.low_stops.iter().copied().fold(f64::INFINITY, f64::min);
146 Some(ChandeKrollStopOutput {
147 stop_long,
148 stop_short,
149 })
150 }
151
152 fn reset(&mut self) {
153 self.atr.reset();
154 self.highs.clear();
155 self.lows.clear();
156 self.high_stops.clear();
157 self.low_stops.clear();
158 }
159
160 #[inline]
161 fn warmup_period(&self) -> usize {
162 self.atr_period + self.stop_period - 1
165 }
166
167 #[inline]
168 fn is_ready(&self) -> bool {
169 self.high_stops.len() == self.stop_period
170 }
171
172 #[inline]
173 fn name(&self) -> &'static str {
174 "ChandeKrollStop"
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use crate::traits::BatchExt;
182 use approx::assert_relative_eq;
183
184 fn c(high: f64, low: f64, close: f64, ts: i64) -> Candle {
185 Candle::new(f64::midpoint(high, low), high, low, close, 1.0, ts).unwrap()
186 }
187
188 #[test]
189 fn reference_values_flat_market() {
190 let candles: Vec<Candle> = (0..20).map(|i| c(11.0, 9.0, 10.0, i)).collect();
194 let mut cks = ChandeKrollStop::new(5, 1.0, 3).unwrap();
195 let last = cks.batch(&candles).into_iter().flatten().last().unwrap();
196 assert_relative_eq!(last.stop_short, 9.0, epsilon = 1e-12);
197 assert_relative_eq!(last.stop_long, 11.0, epsilon = 1e-12);
198 }
199
200 #[test]
201 fn first_emission_matches_warmup_period() {
202 let candles: Vec<Candle> = (0..16)
203 .map(|i| {
204 let base = 100.0 + i as f64;
205 c(base + 1.0, base - 1.0, base, i)
206 })
207 .collect();
208 let mut cks = ChandeKrollStop::new(4, 1.0, 3).unwrap();
209 let out = cks.batch(&candles);
210 assert_eq!(cks.warmup_period(), 6);
211 for (i, v) in out.iter().enumerate().take(5) {
212 assert!(v.is_none(), "index {i} must be None during warmup");
213 }
214 assert!(out[5].is_some(), "first value lands at warmup_period - 1");
215 }
216
217 #[test]
218 fn rejects_invalid_params() {
219 assert!(ChandeKrollStop::new(0, 1.0, 9).is_err());
220 assert!(ChandeKrollStop::new(10, 1.0, 0).is_err());
221 assert!(ChandeKrollStop::new(10, 0.0, 9).is_err());
222 assert!(ChandeKrollStop::new(10, -1.0, 9).is_err());
223 assert!(ChandeKrollStop::new(10, f64::NAN, 9).is_err());
224 }
225
226 #[test]
229 fn accessors_and_metadata() {
230 let s = ChandeKrollStop::new(10, 1.0, 9).unwrap();
231 let (p, m, q) = s.params();
232 assert_eq!(p, 10);
233 assert!((m - 1.0).abs() < 1e-12);
234 assert_eq!(q, 9);
235 assert_eq!(s.name(), "ChandeKrollStop");
236 }
237
238 #[test]
239 fn reset_clears_state() {
240 let candles: Vec<Candle> = (0..40)
241 .map(|i| {
242 let base = 100.0 + i as f64;
243 c(base + 1.0, base - 1.0, base, i)
244 })
245 .collect();
246 let mut cks = ChandeKrollStop::classic();
247 cks.batch(&candles);
248 assert!(cks.is_ready());
249 cks.reset();
250 assert!(!cks.is_ready());
251 assert_eq!(cks.update(candles[0]), None);
252 }
253
254 #[test]
255 fn batch_equals_streaming() {
256 let candles: Vec<Candle> = (0..80)
257 .map(|i| {
258 let mid = 100.0 + (i as f64 * 0.3).sin() * 8.0;
259 c(mid + 1.5, mid - 1.5, mid + 0.5, i)
260 })
261 .collect();
262 let mut a = ChandeKrollStop::classic();
263 let mut b = ChandeKrollStop::classic();
264 assert_eq!(
265 a.batch(&candles),
266 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
267 );
268 }
269}