1use std::collections::VecDeque;
5
6use crate::error::{Error, Result};
7use crate::ohlcv::Candle;
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct KaseDevStopOutput {
14 pub value: f64,
16 pub direction: f64,
18}
19
20fn sample_stddev(sum: f64, sum_sq: f64, count: usize) -> f64 {
22 let n = count as f64;
23 let mean = sum / n;
24 (((sum_sq - n * mean * mean) / (n - 1.0)).max(0.0)).sqrt()
25}
26
27#[derive(Debug, Clone)]
63pub struct KaseDevStop {
64 period: usize,
65 dev: f64,
66 prev: Option<Candle>,
67 window: VecDeque<f64>,
68 sum: f64,
69 sum_sq: f64,
70 direction: f64,
71 extreme: f64,
72 stop: f64,
73 last: Option<KaseDevStopOutput>,
74}
75
76impl KaseDevStop {
77 pub fn new(period: usize, dev: f64) -> Result<Self> {
86 if period < 2 {
87 return Err(Error::InvalidPeriod {
88 message: "Kase DevStop period must be >= 2",
89 });
90 }
91 if !dev.is_finite() || dev <= 0.0 {
92 return Err(Error::NonPositiveMultiplier);
93 }
94 Ok(Self {
95 period,
96 dev,
97 prev: None,
98 window: VecDeque::with_capacity(period),
99 sum: 0.0,
100 sum_sq: 0.0,
101 direction: 0.0,
102 extreme: 0.0,
103 stop: 0.0,
104 last: None,
105 })
106 }
107
108 pub const fn params(&self) -> (usize, f64) {
110 (self.period, self.dev)
111 }
112
113 pub const fn value(&self) -> Option<KaseDevStopOutput> {
115 self.last
116 }
117}
118
119impl Indicator for KaseDevStop {
120 type Input = Candle;
121 type Output = KaseDevStopOutput;
122
123 fn update(&mut self, candle: Candle) -> Option<KaseDevStopOutput> {
124 let Some(prev) = self.prev else {
125 self.prev = Some(candle);
126 return None;
127 };
128 let dtr = candle.high.max(prev.high) - candle.low.min(prev.low);
129 self.prev = Some(candle);
130
131 if self.window.len() == self.period {
132 let old = self.window.pop_front().expect("non-empty");
133 self.sum -= old;
134 self.sum_sq -= old * old;
135 }
136 self.window.push_back(dtr);
137 self.sum += dtr;
138 self.sum_sq += dtr * dtr;
139 if self.window.len() < self.period {
140 return None;
141 }
142 let mean = self.sum / self.period as f64;
143 let band = mean + self.dev * sample_stddev(self.sum, self.sum_sq, self.period);
144
145 if self.direction == 0.0 {
146 self.direction = 1.0;
148 self.extreme = candle.high;
149 self.stop = candle.high - band;
150 } else if self.direction > 0.0 {
151 self.extreme = self.extreme.max(candle.high);
152 let raw = self.extreme - band;
153 self.stop = self.stop.max(raw);
154 if candle.close < self.stop {
155 self.direction = -1.0;
156 self.extreme = candle.low;
157 self.stop = candle.low + band;
158 }
159 } else {
160 self.extreme = self.extreme.min(candle.low);
161 let raw = self.extreme + band;
162 self.stop = self.stop.min(raw);
163 if candle.close > self.stop {
164 self.direction = 1.0;
165 self.extreme = candle.high;
166 self.stop = candle.high - band;
167 }
168 }
169
170 let out = KaseDevStopOutput {
171 value: self.stop,
172 direction: self.direction,
173 };
174 self.last = Some(out);
175 Some(out)
176 }
177
178 fn reset(&mut self) {
179 self.prev = None;
180 self.window.clear();
181 self.sum = 0.0;
182 self.sum_sq = 0.0;
183 self.direction = 0.0;
184 self.extreme = 0.0;
185 self.stop = 0.0;
186 self.last = None;
187 }
188
189 fn warmup_period(&self) -> usize {
190 self.period + 1
191 }
192
193 fn is_ready(&self) -> bool {
194 self.last.is_some()
195 }
196
197 fn name(&self) -> &'static str {
198 "KaseDevStop"
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use crate::traits::BatchExt;
206
207 fn c(high: f64, low: f64, close: f64) -> Candle {
208 Candle::new_unchecked(f64::midpoint(high, low), high, low, close, 1_000.0, 0)
209 }
210
211 #[test]
212 fn rejects_invalid_params() {
213 assert!(matches!(
214 KaseDevStop::new(1, 1.0),
215 Err(Error::InvalidPeriod { .. })
216 ));
217 assert!(matches!(
218 KaseDevStop::new(30, 0.0),
219 Err(Error::NonPositiveMultiplier)
220 ));
221 assert!(matches!(
222 KaseDevStop::new(30, -1.0),
223 Err(Error::NonPositiveMultiplier)
224 ));
225 }
226
227 #[test]
228 fn accessors_and_metadata() {
229 let k = KaseDevStop::new(30, 1.0).unwrap();
230 assert_eq!(k.params(), (30, 1.0));
231 assert_eq!(k.warmup_period(), 31);
232 assert_eq!(k.name(), "KaseDevStop");
233 assert!(!k.is_ready());
234 assert_eq!(k.value(), None);
235 }
236
237 #[test]
238 fn first_emission_at_warmup_period() {
239 let mut k = KaseDevStop::new(3, 1.0).unwrap();
240 let candles: Vec<Candle> = (0..8)
241 .map(|i| {
242 let base = 100.0 + f64::from(i);
243 c(base + 1.0, base - 1.0, base)
244 })
245 .collect();
246 let out = k.batch(&candles);
247 let warmup = k.warmup_period(); assert_eq!(warmup, 4);
249 for v in out.iter().take(warmup - 1) {
250 assert!(v.is_none());
251 }
252 assert!(out[warmup - 1].is_some());
253 }
254
255 #[test]
256 fn uptrend_keeps_stop_below_price() {
257 let mut k = KaseDevStop::new(5, 1.0).unwrap();
258 let candles: Vec<Candle> = (0..60)
259 .map(|i| {
260 let base = 100.0 + 2.0 * f64::from(i);
261 c(base + 1.0, base - 1.0, base + 0.5)
262 })
263 .collect();
264 for (o, candle) in k.batch(&candles).into_iter().zip(candles.iter()) {
265 if let Some(o) = o {
266 assert_eq!(o.direction, 1.0, "pure uptrend stays long");
267 assert!(o.value < candle.close, "stop below price");
268 }
269 }
270 }
271
272 #[test]
273 fn stop_ratchets_up_in_uptrend() {
274 let mut k = KaseDevStop::new(5, 1.0).unwrap();
275 let candles: Vec<Candle> = (0..60)
276 .map(|i| {
277 let base = 100.0 + 2.0 * f64::from(i);
278 c(base + 1.0, base - 1.0, base + 0.5)
279 })
280 .collect();
281 let mut prev = f64::NEG_INFINITY;
282 for o in k.batch(&candles).into_iter().flatten() {
283 assert!(o.value >= prev, "long stop must not fall");
284 prev = o.value;
285 }
286 }
287
288 #[test]
289 fn flips_on_reversal() {
290 let mut candles: Vec<Candle> = (0..40)
291 .map(|i| {
292 let base = 100.0 + f64::from(i);
293 c(base + 1.0, base - 1.0, base + 0.5)
294 })
295 .collect();
296 candles.extend((0..40).map(|i| {
297 let base = 140.0 - f64::from(i);
298 c(base + 1.0, base - 1.0, base - 0.5)
299 }));
300 let mut k = KaseDevStop::new(5, 1.0).unwrap();
301 let dirs: Vec<f64> = k
302 .batch(&candles)
303 .into_iter()
304 .flatten()
305 .map(|o| o.direction)
306 .collect();
307 assert!(dirs.iter().any(|&d| d > 0.0));
308 assert!(dirs.iter().any(|&d| d < 0.0));
309 }
310
311 #[test]
312 fn reset_clears_state() {
313 let mut k = KaseDevStop::new(5, 1.0).unwrap();
314 let candles: Vec<Candle> = (0..40)
315 .map(|i| {
316 let base = 100.0 + f64::from(i);
317 c(base + 1.0, base - 1.0, base + 0.5)
318 })
319 .collect();
320 k.batch(&candles);
321 assert!(k.is_ready());
322 k.reset();
323 assert!(!k.is_ready());
324 assert_eq!(k.value(), None);
325 assert_eq!(k.update(candles[0]), None);
326 }
327
328 #[test]
329 fn batch_equals_streaming() {
330 let candles: Vec<Candle> = (0..120)
331 .map(|i| {
332 let base = 100.0 + (f64::from(i) * 0.25).sin() * 9.0;
333 c(base + 2.0, base - 1.5, base + 0.5)
334 })
335 .collect();
336 let batch = KaseDevStop::new(20, 2.0).unwrap().batch(&candles);
337 let mut b = KaseDevStop::new(20, 2.0).unwrap();
338 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
339 assert_eq!(batch, streamed);
340 }
341}