kestrel_chartkit/indicator/
dpo.rs1use super::{Indicator, IndicatorAlert, IndicatorOutput};
2use crate::model::Bar;
3use std::collections::VecDeque;
4
5#[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}