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 fn update(&mut self, input: f64) -> Option<f64> {
66 let mean = self.sma.update(input)?;
67 if mean == 0.0 {
68 return Some(0.0);
69 }
70 Some(100.0 * (input - mean) / mean)
71 }
72
73 fn reset(&mut self) {
74 self.sma.reset();
75 }
76
77 fn warmup_period(&self) -> usize {
78 self.period
79 }
80
81 fn is_ready(&self) -> bool {
82 self.sma.is_ready()
83 }
84
85 fn name(&self) -> &'static str {
86 "DisparityIndex"
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use crate::traits::BatchExt;
94 use approx::assert_relative_eq;
95
96 #[test]
97 fn rejects_zero_period() {
98 assert!(DisparityIndex::new(0).is_err());
99 }
100
101 #[test]
104 fn accessors_and_metadata() {
105 let di = DisparityIndex::new(14).unwrap();
106 assert_eq!(di.period(), 14);
107 assert_eq!(di.warmup_period(), 14);
108 assert_eq!(di.name(), "DisparityIndex");
109 }
110
111 #[test]
112 fn warmup_then_known_value() {
113 let mut di = DisparityIndex::new(3).unwrap();
115 assert_eq!(di.update(2.0), None);
116 assert_eq!(di.update(4.0), None);
117 assert_relative_eq!(di.update(6.0).unwrap(), 50.0, epsilon = 1e-12);
118 }
119
120 #[test]
121 fn constant_series_is_zero() {
122 let mut di = DisparityIndex::new(5).unwrap();
124 for v in di.batch(&[42.0; 20]).into_iter().flatten() {
125 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
126 }
127 }
128
129 #[test]
130 fn negative_when_below_mean() {
131 let mut di = DisparityIndex::new(3).unwrap();
133 let v = di.batch(&[10.0, 8.0, 6.0]);
134 assert_relative_eq!(v[2].unwrap(), -25.0, epsilon = 1e-12);
135 }
136
137 #[test]
138 fn zero_mean_returns_zero() {
139 let mut di = DisparityIndex::new(2).unwrap();
142 assert_eq!(di.update(-3.0), None);
143 assert_relative_eq!(di.update(3.0).unwrap(), 0.0, epsilon = 1e-12);
145 }
146
147 #[test]
148 fn reset_clears_state() {
149 let mut di = DisparityIndex::new(5).unwrap();
150 di.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
151 assert!(di.is_ready());
152 di.reset();
153 assert!(!di.is_ready());
154 assert_eq!(di.update(1.0), None);
155 }
156
157 #[test]
158 fn batch_equals_streaming() {
159 let prices: Vec<f64> = (1..=30)
160 .map(|i| 50.0 + (f64::from(i) * 0.3).sin() * 10.0)
161 .collect();
162 let mut a = DisparityIndex::new(7).unwrap();
163 let mut b = DisparityIndex::new(7).unwrap();
164 assert_eq!(
165 a.batch(&prices),
166 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
167 );
168 }
169}