quantwave_core/indicators/
choppiness_index.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5#[derive(Debug, Clone)]
11pub struct ChoppinessIndex {
12 period: usize,
13 tr_window: VecDeque<f64>,
14 high_window: VecDeque<f64>,
15 low_window: VecDeque<f64>,
16 prev_close: Option<f64>,
17}
18
19impl ChoppinessIndex {
20 pub fn new(period: usize) -> Self {
21 Self {
22 period,
23 tr_window: VecDeque::with_capacity(period),
24 high_window: VecDeque::with_capacity(period),
25 low_window: VecDeque::with_capacity(period),
26 prev_close: None,
27 }
28 }
29}
30
31impl Default for ChoppinessIndex {
32 fn default() -> Self {
33 Self::new(14)
34 }
35}
36
37impl Next<(f64, f64, f64)> for ChoppinessIndex {
38 type Output = f64; fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
41 let tr = match self.prev_close {
43 None => high - low,
44 Some(pc) => {
45 let h_pc = (high - pc).abs();
46 let l_pc = (low - pc).abs();
47 let h_l = high - low;
48 h_pc.max(l_pc).max(h_l)
49 }
50 };
51 self.prev_close = Some(close);
52
53 self.tr_window.push_front(tr);
54 self.high_window.push_front(high);
55 self.low_window.push_front(low);
56
57 if self.tr_window.len() > self.period {
58 self.tr_window.pop_back();
59 self.high_window.pop_back();
60 self.low_window.pop_back();
61 }
62
63 if self.tr_window.len() < self.period {
64 return 50.0; }
66
67 let sum_tr: f64 = self.tr_window.iter().sum();
69
70 let mut max_h = f64::MIN;
72 let mut min_l = f64::MAX;
73 for &h in &self.high_window { if h > max_h { max_h = h; } }
74 for &l in &self.low_window { if l < min_l { min_l = l; } }
75
76 let range = max_h - min_l;
77
78 if range == 0.0 {
79 100.0
80 } else {
81 let n_f = self.period as f64;
82 100.0 * (sum_tr / range).log10() / n_f.log10()
83 }
84 }
85}
86
87pub const CHOPPINESS_INDEX_METADATA: IndicatorMetadata = IndicatorMetadata {
88 name: "Choppiness Index",
89 description: "Determines if the market is trending (low values) or ranging/choppy (high values).",
90 usage: "Use to determine whether a market is trending or choppy before selecting a trading strategy. Values above 61.8 indicate chop; values below 38.2 indicate a strong trend.",
91 keywords: &["volatility", "trend-strength", "classic", "range"],
92 ehlers_summary: "The Choppiness Index, developed by E.W. Dreiss, measures how much of the total ATR-based range is consumed by the actual net price move over N bars. A value near 100 means price wandered back and forth using all available range without net progress (maximum chop); near 0 means a straight directional move with minimal retracement. — StockCharts ChartSchool",
93 params: &[
94 ParamDef { name: "period", default: "14", description: "Lookback period" },
95 ],
96 formula_source: "https://www.tradingview.com/support/solutions/43000501980-choppiness-index-chop/",
97 formula_latex: r#"
98\[
99CHOP = 100 \times \frac{\log_{10}(\sum_{i=1}^n ATR(1)_i / (\max(H, n) - \min(L, n)))}{\log_{10}(n)}
100\]
101"#,
102 gold_standard_file: "choppiness_index.json",
103 category: "Modern",
104};
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::traits::Next;
110 use proptest::prelude::*;
111
112 #[test]
113 fn test_chop_basic() {
114 let mut chop = ChoppinessIndex::new(14);
115 for i in 0..30 {
116 let val = chop.next((100.0 + i as f64, 90.0 + i as f64, 95.0 + i as f64));
117 assert!(val >= 0.0 && val <= 100.0);
118 }
119 }
120
121 proptest! {
122 #[test]
123 fn test_chop_parity(
124 inputs in prop::collection::vec(1.0..100.0, 50..100),
125 ) {
126 let period = 14;
127 let mut chop = ChoppinessIndex::new(period);
128 let ohlc_inputs: Vec<(f64, f64, f64)> = inputs.iter().map(|&x| (x + 1.0, x - 1.0, x)).collect();
130 let streaming_results: Vec<f64> = ohlc_inputs.iter().map(|&x| chop.next(x)).collect();
131
132 let mut chop_batch = ChoppinessIndex::new(period);
133 let batch_results: Vec<f64> = ohlc_inputs.iter().map(|&x| chop_batch.next(x)).collect();
134
135 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
136 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
137 }
138 }
139 }
140}