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.
6///
7/// `DPO = close_{t - floor(period/2)} - SMA(period)_t`: the close `floor(period/2)` bars ago against
8/// the current simple average of the last `period` closes. The common construction compares the
9/// current close with an average shifted `period/2 + 1` bars back instead; the values differ.
10///
11/// First output: with the `period`-th bar. [`Indicator::reset`] clears the window.
12#[derive(Debug, Clone)]
13pub struct DpoEngine {
14    period: usize,
15    window: VecDeque<f64>,
16}
17
18impl DpoEngine {
19    pub fn new(period: usize) -> Self {
20        Self {
21            period: period.max(2),
22            window: VecDeque::with_capacity(period + 1),
23        }
24    }
25}
26
27impl Indicator for DpoEngine {
28    fn name(&self) -> &str {
29        "dpo"
30    }
31
32    fn warmup_period(&self) -> usize {
33        self.period
34    }
35
36    fn reset(&mut self) {
37        self.window.clear();
38    }
39
40    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
41        self.window.push_back(bar.close);
42        if self.window.len() > self.period {
43            self.window.pop_front();
44        }
45
46        if self.window.len() < self.period {
47            return None;
48        }
49
50        let sma = self.window.iter().sum::<f64>() / self.period as f64;
51        let lookback_offset = self.period / 2 + 1;
52
53        let past_price = if self.window.len() >= lookback_offset {
54            self.window[self.window.len() - lookback_offset]
55        } else {
56            bar.close
57        };
58
59        let dpo_val = past_price - sma;
60        Some(IndicatorOutput::new(dpo_val))
61    }
62
63    fn alerts(&self) -> Vec<IndicatorAlert> {
64        Vec::new()
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn test_dpo_basic() {
74        let mut dpo = DpoEngine::new(20);
75        let mut out = None;
76        for i in 0..30 {
77            let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0 + i as f64, 1000.0);
78            out = dpo.on_bar(&b);
79        }
80        assert!(out.is_some());
81    }
82}