wickra_core/indicators/
disparity_index.rs1use crate::error::Result;
4use crate::indicators::sma::Sma;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
37pub struct DisparityIndex {
38 period: usize,
39 sma: Sma,
40}
41
42impl DisparityIndex {
43 pub fn new(period: usize) -> Result<Self> {
49 Ok(Self {
50 period,
51 sma: Sma::new(period)?,
52 })
53 }
54
55 pub const fn period(&self) -> usize {
57 self.period
58 }
59}
60
61impl Indicator for DisparityIndex {
62 type Input = f64;
63 type Output = f64;
64
65 #[inline]
66 fn update(&mut self, input: f64) -> Option<f64> {
67 let mean = self.sma.update(input)?;
68 if mean == 0.0 {
69 return Some(0.0);
70 }
71 Some(100.0 * (input - mean) / mean)
72 }
73
74 fn reset(&mut self) {
75 self.sma.reset();
76 }
77
78 #[inline]
79 fn warmup_period(&self) -> usize {
80 self.period
81 }
82
83 #[inline]
84 fn is_ready(&self) -> bool {
85 self.sma.is_ready()
86 }
87
88 #[inline]
89 fn name(&self) -> &'static str {
90 "DisparityIndex"
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::traits::BatchExt;
98 use approx::assert_relative_eq;
99
100 #[test]
101 fn rejects_zero_period() {
102 assert!(DisparityIndex::new(0).is_err());
103 }
104
105 #[test]
108 fn accessors_and_metadata() {
109 let di = DisparityIndex::new(14).unwrap();
110 assert_eq!(di.period(), 14);
111 assert_eq!(di.warmup_period(), 14);
112 assert_eq!(di.name(), "DisparityIndex");
113 }
114
115 #[test]
116 fn warmup_then_known_value() {
117 let mut di = DisparityIndex::new(3).unwrap();
119 assert_eq!(di.update(2.0), None);
120 assert_eq!(di.update(4.0), None);
121 assert_relative_eq!(di.update(6.0).unwrap(), 50.0, epsilon = 1e-12);
122 }
123
124 #[test]
125 fn constant_series_is_zero() {
126 let mut di = DisparityIndex::new(5).unwrap();
128 for v in di.batch(&[42.0; 20]).into_iter().flatten() {
129 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
130 }
131 }
132
133 #[test]
134 fn negative_when_below_mean() {
135 let mut di = DisparityIndex::new(3).unwrap();
137 let v = di.batch(&[10.0, 8.0, 6.0]);
138 assert_relative_eq!(v[2].unwrap(), -25.0, epsilon = 1e-12);
139 }
140
141 #[test]
142 fn zero_mean_returns_zero() {
143 let mut di = DisparityIndex::new(2).unwrap();
146 assert_eq!(di.update(-3.0), None);
147 assert_relative_eq!(di.update(3.0).unwrap(), 0.0, epsilon = 1e-12);
149 }
150
151 #[test]
152 fn reset_clears_state() {
153 let mut di = DisparityIndex::new(5).unwrap();
154 di.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
155 assert!(di.is_ready());
156 di.reset();
157 assert!(!di.is_ready());
158 assert_eq!(di.update(1.0), None);
159 }
160
161 #[test]
162 fn batch_equals_streaming() {
163 let prices: Vec<f64> = (1..=30)
164 .map(|i| 50.0 + (f64::from(i) * 0.3).sin() * 10.0)
165 .collect();
166 let mut a = DisparityIndex::new(7).unwrap();
167 let mut b = DisparityIndex::new(7).unwrap();
168 assert_eq!(
169 a.batch(&prices),
170 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
171 );
172 }
173}