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