kestrel_chartkit/indicator/
choppiness.rs1use super::{Indicator, IndicatorAlert, IndicatorOutput};
2use crate::model::Bar;
3use std::collections::VecDeque;
4
5#[derive(Debug, Clone)]
14pub struct ChoppinessIndexEngine {
15 period: usize,
16 prev_close: Option<f64>,
17 tr_sum_window: VecDeque<f64>,
18 bars: VecDeque<Bar>,
19}
20
21impl ChoppinessIndexEngine {
22 pub fn new(period: usize) -> Self {
23 Self {
24 period: period.max(2),
25 prev_close: None,
26 tr_sum_window: VecDeque::with_capacity(period),
27 bars: VecDeque::with_capacity(period),
28 }
29 }
30}
31
32impl Indicator for ChoppinessIndexEngine {
33 fn name(&self) -> &str {
34 "choppiness"
35 }
36
37 fn warmup_period(&self) -> usize {
38 self.period
39 }
40
41 fn reset(&mut self) {
42 self.prev_close = None;
43 self.tr_sum_window.clear();
44 self.bars.clear();
45 }
46
47 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
48 let tr = match self.prev_close {
49 Some(prev_c) => (bar.high - bar.low)
50 .max((bar.high - prev_c).abs())
51 .max((bar.low - prev_c).abs()),
52 None => bar.high - bar.low,
53 };
54 self.prev_close = Some(bar.close);
55
56 self.tr_sum_window.push_back(tr);
57 self.bars.push_back(bar.clone());
58
59 if self.tr_sum_window.len() > self.period {
60 self.tr_sum_window.pop_front();
61 self.bars.pop_front();
62 }
63
64 if self.tr_sum_window.len() < self.period {
65 return None;
66 }
67
68 let sum_tr: f64 = self.tr_sum_window.iter().sum();
69 let max_h = self.bars.iter().map(|b| b.high).fold(f64::MIN, f64::max);
70 let min_l = self.bars.iter().map(|b| b.low).fold(f64::MAX, f64::min);
71 let range = (max_h - min_l).max(1e-8);
72
73 let n_f64 = self.period as f64;
74 let chop = 100.0 * (sum_tr / range).log10() / n_f64.log10();
75 let chop_clamped = chop.clamp(0.0, 100.0);
76
77 Some(IndicatorOutput::new(chop_clamped))
78 }
79
80 fn alerts(&self) -> Vec<IndicatorAlert> {
81 Vec::new()
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn test_choppiness_index() {
91 let mut chop = ChoppinessIndexEngine::new(14);
92 let mut out = None;
93 for i in 0..30 {
94 let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0 + (i % 2) as f64, 1000.0);
95 out = chop.on_bar(&b);
96 }
97 assert!(out.is_some());
98 let val = out.unwrap().value;
99 assert!((0.0..=100.0).contains(&val));
100 }
101}