Skip to main content

egui_charts/studies/builtin/
price_channel.rs

1use super::over_finite_bars;
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::stepper::{update_window, usable_behind};
5use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
6use egui::Color32;
7
8/// Price Channel
9/// Simple highest high and lowest low channel
10/// Also known as "N-bar high/low"
11#[derive(Clone)]
12pub struct PriceChannel {
13    /// Lookback period
14    period: usize,
15    offset: i32,
16    values: IndicatorValues,
17    /// Colors: [Upper, Lower, Middle]
18    colors: Vec<Color32>,
19    visible: bool,
20}
21
22impl PriceChannel {
23    /// Create with a lookback of `period` bars, clamped to at least 1.
24    pub fn new(period: usize) -> Self {
25        Self {
26            period: period.max(1),
27            offset: 0,
28            values: IndicatorValues::new(),
29            colors: vec![
30                Color32::from_rgb(245, 0, 87), // Upper
31                Color32::from_rgb(245, 0, 87), // Lower
32                palette::LIGHT_BLUE,           // Middle
33            ],
34            visible: true,
35        }
36    }
37
38    /// Set plot colors in the order returned by [`Indicator::line_names`].
39    pub fn with_colors(mut self, colors: Vec<Color32>) -> Self {
40        self.colors = colors;
41        self
42    }
43
44    /// Draw every line `offset` bars later, earlier when negative.
45    pub fn with_offset(mut self, offset: i32) -> Self {
46        self.offset = offset;
47        self
48    }
49}
50
51impl Default for PriceChannel {
52    fn default() -> Self {
53        Self::new(20)
54    }
55}
56
57impl PriceChannel {
58    /// The values over `data`, every bar of which has finite inputs.
59    fn compute(&self, data: &[Bar]) -> IndicatorValues {
60        let mut values = IndicatorValues::new();
61        if data.len() < self.period {
62            for _ in 0..data.len() {
63                values.push(IndicatorValue::None);
64            }
65            return values;
66        }
67
68        for i in 0..data.len() {
69            if i < self.period - 1 {
70                values.push(IndicatorValue::None);
71            } else {
72                let window = &data[i + 1 - self.period..=i];
73                let highest = window
74                    .iter()
75                    .map(|b| b.high)
76                    .fold(f64::NEG_INFINITY, f64::max);
77                let lowest = window.iter().map(|b| b.low).fold(f64::INFINITY, f64::min);
78                let middle = (highest + lowest) / 2.0;
79
80                values.push_lines(&[highest, lowest, middle]);
81            }
82        }
83
84        values
85    }
86}
87
88impl Indicator for PriceChannel {
89    crate::studies::inputs::study_inputs!(self;
90        period: length("Period", 1, usize::MAX),
91        offset: integer("Offset", i32::MIN, i32::MAX)
92    ; {});
93
94    fn plot_offset(&self, _line: usize) -> isize {
95        self.offset as isize
96    }
97
98    fn name(&self) -> &str {
99        "Price Channel"
100    }
101
102    fn desc(&self) -> &str {
103        "Price Channel - Highest high / Lowest low bands"
104    }
105
106    fn calculate(&mut self, data: &[Bar]) {
107        self.update(data, 0);
108    }
109
110    fn update(&mut self, data: &[Bar], from: usize) {
111        let mut values = std::mem::take(&mut self.values);
112        update_window(
113            &mut values,
114            data,
115            from,
116            usable_behind(self.period - 1, |b| b.high.is_finite() && b.low.is_finite()),
117            |data| {
118                over_finite_bars(
119                    data,
120                    |b| b.high.is_finite() && b.low.is_finite(),
121                    |data| self.compute(data),
122                )
123            },
124        );
125        self.values = values;
126    }
127
128    fn is_incremental(&self) -> bool {
129        true
130    }
131
132    fn values(&self) -> &IndicatorValues {
133        &self.values
134    }
135
136    fn colors(&self) -> Vec<Color32> {
137        self.colors.clone()
138    }
139
140    fn set_colors(&mut self, colors: Vec<Color32>) {
141        if colors.len() >= 3 {
142            self.colors = colors;
143        }
144    }
145
146    fn is_overlay(&self) -> bool {
147        true // Overlay on price chart
148    }
149
150    fn is_visible(&self) -> bool {
151        self.visible
152    }
153
154    fn set_visible(&mut self, visible: bool) {
155        self.visible = visible;
156    }
157
158    fn clone_box(&self) -> Box<dyn Indicator> {
159        Box::new(self.clone())
160    }
161
162    fn line_names(&self) -> Vec<String> {
163        vec![
164            format!("Upper({})", self.period),
165            format!("Lower({})", self.period),
166            "Middle".to_string(),
167        ]
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use crate::studies::test_support::make_range_bar as make_bar;
175
176    #[test]
177    fn test_price_channel() {
178        let mut pc = PriceChannel::new(5);
179
180        let data = vec![
181            make_bar(102.0, 98.0, 100.0),
182            make_bar(104.0, 99.0, 103.0),
183            make_bar(106.0, 101.0, 105.0),
184            make_bar(108.0, 103.0, 107.0),
185            make_bar(110.0, 105.0, 109.0),
186        ];
187
188        pc.calculate(&data);
189
190        assert_eq!(pc.values.len(), 5);
191
192        // Last value should be highest=110, lowest=98, middle=104
193        let rows = pc.values.to_rows();
194        let IndicatorValue::Multiple(v) = rows.last().unwrap() else {
195            panic!("no value: {:?}", pc.values.to_rows().last().unwrap());
196        };
197        assert_eq!(v.len(), 3);
198        assert!((v[0] - 110.0).abs() < 0.01); // Upper
199        assert!((v[1] - 98.0).abs() < 0.01); // Lower
200        assert!((v[2] - 104.0).abs() < 0.01); // Middle
201    }
202}