use std::collections::HashMap;
use crate::error::IndicatorError;
use crate::indicator::{Indicator, IndicatorOutput};
use crate::registry::param_usize;
use crate::types::Candle;
#[derive(Debug, Clone)]
pub struct ChopParams {
pub period: usize,
}
impl Default for ChopParams {
fn default() -> Self {
Self { period: 14 }
}
}
#[derive(Debug, Clone)]
pub struct ChoppinessIndex {
pub params: ChopParams,
}
impl ChoppinessIndex {
pub fn new(params: ChopParams) -> Self {
Self { params }
}
pub fn with_period(period: usize) -> Self {
Self::new(ChopParams { period })
}
fn output_key(&self) -> String {
format!("CHOP_{}", self.params.period)
}
}
impl Indicator for ChoppinessIndex {
fn name(&self) -> &'static str {
"ChoppinessIndex"
}
fn required_len(&self) -> usize {
self.params.period
}
fn required_columns(&self) -> &[&'static str] {
&["high", "low"]
}
fn calculate(&self, candles: &[Candle]) -> Result<IndicatorOutput, IndicatorError> {
self.check_len(candles)?;
let n = candles.len();
let p = self.params.period;
let log_period = (p as f64).log10();
let mut values = vec![f64::NAN; n];
for i in (p - 1)..n {
let window = &candles[(i + 1 - p)..=i];
let atr_sum: f64 = window.iter().map(|c| c.high - c.low).sum();
let max_h = window
.iter()
.map(|c| c.high)
.fold(f64::NEG_INFINITY, f64::max);
let min_l = window.iter().map(|c| c.low).fold(f64::INFINITY, f64::min);
let denom = max_h - min_l;
values[i] = if denom == 0.0 || log_period == 0.0 {
f64::NAN
} else {
100.0 * (atr_sum / denom).log10() / log_period
};
}
Ok(IndicatorOutput::from_pairs([(self.output_key(), values)]))
}
}
pub fn factory<S: ::std::hash::BuildHasher>(params: &HashMap<String, String, S>) -> Result<Box<dyn Indicator>, IndicatorError> {
Ok(Box::new(ChoppinessIndex::new(ChopParams {
period: param_usize(params, "period", 14)?,
})))
}
#[cfg(test)]
mod tests {
use super::*;
fn candles(n: usize, range: f64) -> Vec<Candle> {
(0..n)
.map(|i| Candle {
time: i64::try_from(i).expect("time index fits i64"),
open: 10.0,
high: 10.0 + range,
low: 10.0 - range,
close: 10.0,
volume: 100.0,
})
.collect()
}
#[test]
fn chop_output_column() {
let out = ChoppinessIndex::with_period(14)
.calculate(&candles(20, 1.0))
.unwrap();
assert!(out.get("CHOP_14").is_some());
}
#[test]
fn chop_constant_range_near_100() {
let out = ChoppinessIndex::with_period(14)
.calculate(&candles(20, 1.0))
.unwrap();
let vals = out.get("CHOP_14").unwrap();
let last = vals.iter().rev().find(|v| !v.is_nan()).copied().unwrap();
assert!((last - 100.0).abs() < 1e-6, "got {last}");
}
#[test]
fn factory_creates_chop() {
assert_eq!(factory(&HashMap::new()).unwrap().name(), "ChoppinessIndex");
}
}