indicators/volatility/
choppiness_index.rs1use std::collections::HashMap;
20
21use crate::error::IndicatorError;
22use crate::indicator::{Indicator, IndicatorOutput};
23use crate::registry::param_usize;
24use crate::types::Candle;
25
26#[derive(Debug, Clone)]
27pub struct ChopParams {
28 pub period: usize,
29}
30impl Default for ChopParams {
31 fn default() -> Self {
32 Self { period: 14 }
33 }
34}
35
36#[derive(Debug, Clone)]
37pub struct ChoppinessIndex {
38 pub params: ChopParams,
39}
40
41impl ChoppinessIndex {
42 pub fn new(params: ChopParams) -> Self {
43 Self { params }
44 }
45 pub fn with_period(period: usize) -> Self {
46 Self::new(ChopParams { period })
47 }
48 fn output_key(&self) -> String {
49 format!("CHOP_{}", self.params.period)
50 }
51}
52
53impl Indicator for ChoppinessIndex {
54 fn name(&self) -> &'static str {
55 "ChoppinessIndex"
56 }
57 fn required_len(&self) -> usize {
58 self.params.period
59 }
60 fn required_columns(&self) -> &[&'static str] {
61 &["high", "low"]
62 }
63
64 fn calculate(&self, candles: &[Candle]) -> Result<IndicatorOutput, IndicatorError> {
69 self.check_len(candles)?;
70
71 let n = candles.len();
72 let p = self.params.period;
73 let log_period = (p as f64).log10();
74
75 let mut values = vec![f64::NAN; n];
76
77 for i in (p - 1)..n {
78 let window = &candles[(i + 1 - p)..=i];
79 let atr_sum: f64 = window.iter().map(|c| c.high - c.low).sum();
80 let max_h = window
81 .iter()
82 .map(|c| c.high)
83 .fold(f64::NEG_INFINITY, f64::max);
84 let min_l = window.iter().map(|c| c.low).fold(f64::INFINITY, f64::min);
85 let denom = max_h - min_l;
86 values[i] = if denom == 0.0 || log_period == 0.0 {
87 f64::NAN
88 } else {
89 100.0 * (atr_sum / denom).log10() / log_period
90 };
91 }
92
93 Ok(IndicatorOutput::from_pairs([(self.output_key(), values)]))
94 }
95}
96
97pub fn factory<S: ::std::hash::BuildHasher>(
98 params: &HashMap<String, String, S>,
99) -> Result<Box<dyn Indicator>, IndicatorError> {
100 Ok(Box::new(ChoppinessIndex::new(ChopParams {
101 period: param_usize(params, "period", 14)?,
102 })))
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 fn candles(n: usize, range: f64) -> Vec<Candle> {
110 (0..n)
111 .map(|i| Candle {
112 time: i64::try_from(i).expect("time index fits i64"),
113 open: 10.0,
114 high: 10.0 + range,
115 low: 10.0 - range,
116 close: 10.0,
117 volume: 100.0,
118 })
119 .collect()
120 }
121
122 #[test]
123 fn chop_output_column() {
124 let out = ChoppinessIndex::with_period(14)
125 .calculate(&candles(20, 1.0))
126 .unwrap();
127 assert!(out.get("CHOP_14").is_some());
128 }
129
130 #[test]
131 fn chop_constant_range_near_100() {
132 let out = ChoppinessIndex::with_period(14)
137 .calculate(&candles(20, 1.0))
138 .unwrap();
139 let vals = out.get("CHOP_14").unwrap();
140 let last = vals.iter().rev().find(|v| !v.is_nan()).copied().unwrap();
141 assert!((last - 100.0).abs() < 1e-6, "got {last}");
142 }
143
144 #[test]
145 fn factory_creates_chop() {
146 assert_eq!(factory(&HashMap::new()).unwrap().name(), "ChoppinessIndex");
147 }
148}