wickra_core/indicators/
dpo.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
38pub struct Dpo {
39 period: usize,
40 shift: usize,
41 capacity: usize,
43 window: VecDeque<f64>,
44 sum: f64,
45 last: Option<f64>,
46}
47
48impl Dpo {
49 pub fn new(period: usize) -> Result<Self> {
55 if period == 0 {
56 return Err(Error::PeriodZero);
57 }
58 let shift = period / 2 + 1;
59 let capacity = period.max(shift + 1);
62 Ok(Self {
63 period,
64 shift,
65 capacity,
66 window: VecDeque::with_capacity(capacity),
67 sum: 0.0,
68 last: None,
69 })
70 }
71
72 pub const fn period(&self) -> usize {
74 self.period
75 }
76
77 pub const fn shift(&self) -> usize {
79 self.shift
80 }
81
82 pub const fn value(&self) -> Option<f64> {
84 self.last
85 }
86}
87
88impl Indicator for Dpo {
89 type Input = f64;
90 type Output = f64;
91
92 fn update(&mut self, input: f64) -> Option<f64> {
93 if !input.is_finite() {
94 return self.last;
96 }
97 self.window.push_back(input);
98 self.sum += input;
99 let len = self.window.len();
100 if len > self.period {
101 self.sum -= self.window[len - 1 - self.period];
103 }
104 if self.window.len() > self.capacity {
105 self.window.pop_front();
106 }
107 if self.window.len() < self.capacity {
108 return None;
109 }
110 let sma = self.sum / self.period as f64;
111 let shifted = self.window[self.window.len() - 1 - self.shift];
113 let dpo = shifted - sma;
114 self.last = Some(dpo);
115 Some(dpo)
116 }
117
118 fn reset(&mut self) {
119 self.window.clear();
120 self.sum = 0.0;
121 self.last = None;
122 }
123
124 fn warmup_period(&self) -> usize {
125 self.capacity
126 }
127
128 fn is_ready(&self) -> bool {
129 self.last.is_some()
130 }
131
132 fn name(&self) -> &'static str {
133 "DPO"
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::traits::BatchExt;
141 use approx::assert_relative_eq;
142
143 #[test]
144 fn new_rejects_zero_period() {
145 assert!(matches!(Dpo::new(0), Err(Error::PeriodZero)));
146 }
147
148 #[test]
153 fn accessors_and_metadata() {
154 let mut dpo = Dpo::new(20).unwrap();
155 assert_eq!(dpo.period(), 20);
156 assert_eq!(dpo.name(), "DPO");
157 assert_eq!(dpo.value(), None);
158 for i in 1..=dpo.warmup_period() {
159 dpo.update(f64::from(u32::try_from(i).unwrap()));
160 }
161 assert!(dpo.value().is_some());
162 }
163
164 #[test]
165 fn shift_is_half_period_plus_one() {
166 assert_eq!(Dpo::new(20).unwrap().shift(), 11);
167 assert_eq!(Dpo::new(4).unwrap().shift(), 3);
168 }
169
170 #[test]
171 fn reference_values() {
172 let mut dpo = Dpo::new(4).unwrap();
175 let out = dpo.batch(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
176 assert_eq!(dpo.warmup_period(), 4);
177 assert_eq!(out[0], None);
178 assert_eq!(out[2], None);
179 assert_relative_eq!(out[3].unwrap(), -1.5, epsilon = 1e-12);
180 assert_relative_eq!(out[4].unwrap(), -1.5, epsilon = 1e-12);
181 assert_relative_eq!(out[5].unwrap(), -1.5, epsilon = 1e-12);
182 }
183
184 #[test]
185 fn constant_series_yields_zero() {
186 let mut dpo = Dpo::new(10).unwrap();
188 let out = dpo.batch(&[50.0; 40]);
189 for v in out.iter().skip(dpo.warmup_period() - 1).flatten() {
190 assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
191 }
192 }
193
194 #[test]
195 fn ignores_non_finite_input() {
196 let mut dpo = Dpo::new(4).unwrap();
197 let out = dpo.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
198 let last = *out.last().unwrap();
199 assert!(last.is_some());
200 assert_eq!(dpo.update(f64::NAN), last);
201 assert_eq!(dpo.update(f64::INFINITY), last);
202 }
203
204 #[test]
205 fn reset_clears_state() {
206 let mut dpo = Dpo::new(4).unwrap();
207 dpo.batch(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
208 assert!(dpo.is_ready());
209 dpo.reset();
210 assert!(!dpo.is_ready());
211 assert_eq!(dpo.update(1.0), None);
212 }
213
214 #[test]
215 fn batch_equals_streaming() {
216 let prices: Vec<f64> = (1..=80)
217 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 7.0)
218 .collect();
219 let batch = Dpo::new(20).unwrap().batch(&prices);
220 let mut b = Dpo::new(20).unwrap();
221 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
222 assert_eq!(batch, streamed);
223 }
224}