Skip to main content

quantwave_core/indicators/
choppiness_index.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5/// Choppiness Index
6///
7/// The Choppiness Index is a volatility indicator used to determine if the 
8/// market is trending or ranging (choppy).
9/// Values above 61.8 indicate choppiness, while values below 38.2 indicate trending.
10#[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; // Choppiness value
39
40    fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
41        // True Range calculation
42        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; // Neutral value during startup
65        }
66
67        // sum(TrueRange, N)
68        let sum_tr: f64 = self.tr_window.iter().sum();
69        
70        // MaxHigh(N) - MinLow(N)
71        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            // Mock H/L/C from single value
129            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}