Skip to main content

kestrel_chartkit/indicator/
dpo.rs

1use super::{Indicator, IndicatorAlert, IndicatorOutput};
2use crate::model::Bar;
3use std::collections::VecDeque;
4
5/// Detrended Price Oscillator (DPO) Engine.
6/// DPO = Close[N/2 + 1] - SMA(Close, N)
7#[derive(Debug, Clone)]
8pub struct DpoEngine {
9    period: usize,
10    window: VecDeque<f64>,
11}
12
13impl DpoEngine {
14    pub fn new(period: usize) -> Self {
15        Self {
16            period: period.max(2),
17            window: VecDeque::with_capacity(period + 1),
18        }
19    }
20}
21
22impl Indicator for DpoEngine {
23    fn name(&self) -> &str {
24        "dpo"
25    }
26
27    fn warmup_period(&self) -> usize {
28        self.period
29    }
30
31    fn reset(&mut self) {
32        self.window.clear();
33    }
34
35    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
36        self.window.push_back(bar.close);
37        if self.window.len() > self.period {
38            self.window.pop_front();
39        }
40
41        if self.window.len() < self.period {
42            return None;
43        }
44
45        let sma = self.window.iter().sum::<f64>() / self.period as f64;
46        let lookback_offset = self.period / 2 + 1;
47
48        let past_price = if self.window.len() >= lookback_offset {
49            self.window[self.window.len() - lookback_offset]
50        } else {
51            bar.close
52        };
53
54        let dpo_val = past_price - sma;
55        Some(IndicatorOutput::new(dpo_val))
56    }
57
58    fn alerts(&self) -> Vec<IndicatorAlert> {
59        Vec::new()
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn test_dpo_basic() {
69        let mut dpo = DpoEngine::new(20);
70        let mut out = None;
71        for i in 0..30 {
72            let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0 + i as f64, 1000.0);
73            out = dpo.on_bar(&b);
74        }
75        assert!(out.is_some());
76    }
77}