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