egui_charts/studies/builtin/
dpo.rs1use super::over_finite_bars;
34use crate::model::Bar;
35use crate::studies::palette;
36use crate::studies::stepper::{rolling_mean, update_window, usable_behind};
37use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
38use egui::Color32;
39
40#[derive(Clone)]
42pub struct DetrendedPriceOscillator {
43 period: usize,
44 centered: bool,
45 values: IndicatorValues,
46 color: Color32,
47 visible: bool,
48}
49
50impl DetrendedPriceOscillator {
51 pub fn new(period: usize) -> Self {
53 Self {
54 period: period.max(1),
55 centered: false,
56 values: IndicatorValues::new(),
57 color: palette::DARK_GREEN,
58 visible: true,
59 }
60 }
61
62 pub fn with_centered(mut self, centered: bool) -> Self {
64 self.centered = centered;
65 self
66 }
67
68 pub fn with_color(mut self, color: Color32) -> Self {
70 self.color = color;
71 self
72 }
73
74 fn shift(&self) -> usize {
75 self.period / 2 + 1
76 }
77
78 fn finite_compute(&self, data: &[Bar]) -> IndicatorValues {
80 let closes: Vec<f64> = data.iter().map(|b| b.close).collect();
81 let average = rolling_mean(&closes, self.period);
82 (0..data.len())
83 .map(|i| {
84 IndicatorValue::Single(match i.checked_sub(self.shift()) {
85 Some(back) if self.centered => closes[back] - average[i],
86 Some(back) => closes[i] - average[back],
87 None => f64::NAN,
88 })
89 })
90 .collect()
91 }
92}
93
94impl Default for DetrendedPriceOscillator {
96 fn default() -> Self {
97 Self::new(21)
98 }
99}
100
101impl DetrendedPriceOscillator {
102 fn replay(&self, data: &[Bar]) -> IndicatorValues {
103 let mut values = over_finite_bars(
104 data,
105 |b| b.close.is_finite(),
106 |data| self.finite_compute(data),
107 );
108 if self.centered {
109 let mut skipped = None;
111 values = (0..data.len())
112 .map(|i| {
113 if !data[i].close.is_finite() {
114 skipped = Some(i);
115 }
116 let near = skipped.is_some_and(|s| i - s <= self.shift());
117 IndicatorValue::Single(if near {
118 f64::NAN
119 } else {
120 values.value(0, i).unwrap_or(f64::NAN)
121 })
122 })
123 .collect();
124 }
125 values
126 }
127}
128
129impl Indicator for DetrendedPriceOscillator {
130 fn source(&self) -> Option<crate::studies::StudySource> {
131 Some(crate::studies::StudySource::Price(
132 crate::model::PriceSource::Close,
133 ))
134 }
135
136 crate::studies::inputs::study_inputs!(self;
137 period: length("Length", 1, usize::MAX),
138 centered: toggle("Centered")
139 ; {});
140
141 fn plot_offset(&self, _line: usize) -> isize {
142 if self.centered {
143 -(self.shift().min(isize::MAX as usize) as isize)
144 } else {
145 0
146 }
147 }
148
149 fn value_format(&self) -> crate::scales::StudyFormat {
150 crate::scales::StudyFormat::Price
151 }
152
153 fn name(&self) -> &str {
154 "DPO"
155 }
156
157 fn desc(&self) -> &str {
158 "Detrended Price Oscillator - Removes trend to identify cycles"
159 }
160
161 fn calculate(&mut self, data: &[Bar]) {
162 self.update(data, 0);
163 }
164
165 fn update(&mut self, data: &[Bar], from: usize) {
166 let mut values = std::mem::take(&mut self.values);
167 update_window(
168 &mut values,
169 data,
170 from,
171 usable_behind(
172 self.period.saturating_sub(1).saturating_add(self.shift()),
173 |b| b.close.is_finite(),
174 ),
175 |data| self.replay(data),
176 );
177 self.values = values;
178 }
179
180 fn is_incremental(&self) -> bool {
181 true
182 }
183
184 fn values(&self) -> &IndicatorValues {
185 &self.values
186 }
187
188 fn colors(&self) -> Vec<Color32> {
189 vec![self.color]
190 }
191
192 fn set_colors(&mut self, colors: Vec<Color32>) {
193 if !colors.is_empty() {
194 self.color = colors[0];
195 }
196 }
197
198 fn pane(&self) -> crate::studies::StudyPane {
199 crate::studies::StudyPane {
200 levels: vec![(0.0, palette::GRAY, "Zero")],
201 ..Default::default()
202 }
203 }
204
205 fn is_overlay(&self) -> bool {
206 false }
208
209 fn is_visible(&self) -> bool {
210 self.visible
211 }
212
213 fn set_visible(&mut self, visible: bool) {
214 self.visible = visible;
215 }
216
217 fn clone_box(&self) -> Box<dyn Indicator> {
218 Box::new(self.clone())
219 }
220
221 fn line_names(&self) -> Vec<String> {
222 vec![format!("DPO({})", self.period)]
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use chrono::{TimeZone, Utc};
230
231 #[test]
235 fn centred_values_stay_under_their_close_past_a_skipped_bar() {
236 let bars: Vec<Bar> = [0.0, 1.0, 4.0, f64::NAN, 9.0, 16.0]
237 .iter()
238 .enumerate()
239 .map(|(i, &c)| {
240 Bar::new(
241 Utc.timestamp_opt(i as i64 * 60, 0).unwrap(),
242 c,
243 c,
244 c,
245 c,
246 1.0,
247 )
248 })
249 .collect();
250 let mut dpo = DetrendedPriceOscillator::new(1).with_centered(true);
251 dpo.calculate(&bars);
252 let drawn: Vec<_> = (0..5).map(|bar| dpo.plot_value(0, bar)).collect();
253 assert_eq!(drawn, [Some(-1.0), Some(-3.0), None, None, Some(-7.0)]);
254 }
255}