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