Skip to main content

egui_charts/studies/builtin/
ultimate_oscillator.rs

1//! Ultimate Oscillator (UO) Indicator
2//!
3//! Developed by Larry Williams, the Ultimate Oscillator combines short, medium,
4//! and long-term price action into a single oscillator, reducing false signals.
5//!
6//! # Formula
7//! BP (Buying Pressure) = Close - Min(Low, Previous Close)
8//! TR (True Range) = Max(High, Previous Close) - Min(Low, Previous Close)
9//! Avg = (4 * Avg7) + (2 * Avg14) + Avg28
10//! UO = 100 * Avg / 7
11//!
12//! # Interpretation
13//! - Above 70: Overbought
14//! - Below 30: Oversold
15//! - Bullish divergence: Price makes lower low, UO makes higher low
16//!
17//! # Example
18//! ```
19//! # let bars: Vec<egui_charts::model::Bar> = (0..60).map(|i| { let p = 100.0 + (i as f64 * 0.3).sin() * 5.0; egui_charts::model::Bar::new(chrono::Utc::now(), p, p + 1.0, p - 1.0, p + 0.5, 1_000.0) }).collect();
20//! use egui_charts::studies::{UltimateOscillator, Indicator};
21//!
22//! let mut uo = UltimateOscillator::new(7, 14, 28);
23//! uo.calculate(&bars);
24//! ```
25
26use super::over_finite_bars;
27use crate::model::Bar;
28use crate::studies::stepper::{update_window, usable_behind};
29use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
30use egui::Color32;
31
32/// Ultimate Oscillator indicator
33#[derive(Clone)]
34pub struct UltimateOscillator {
35    period1: usize, // Short (typically 7)
36    period2: usize, // Medium (typically 14)
37    period3: usize, // Long (typically 28)
38    values: IndicatorValues,
39    color: Color32,
40    visible: bool,
41}
42
43impl UltimateOscillator {
44    /// Create a new Ultimate Oscillator
45    ///
46    /// # Arguments
47    /// * `period1` - Short period (typically 7)
48    /// * `period2` - Medium period (typically 14)
49    /// * `period3` - Long period (typically 28)
50    pub fn new(period1: usize, period2: usize, period3: usize) -> Self {
51        Self {
52            period1: period1.max(1),
53            period2: period2.max(1),
54            period3: period3.max(1),
55            values: IndicatorValues::new(),
56            color: crate::studies::palette::BRIGHT_RED,
57            visible: true,
58        }
59    }
60
61    /// Create with default params (7, 14, 28)
62    pub fn default_params() -> Self {
63        Self::new(7, 14, 28)
64    }
65
66    /// Set the line color
67    pub fn with_color(mut self, color: Color32) -> Self {
68        self.color = color;
69        self
70    }
71}
72
73/// Construct with the conventional default parameters.
74impl Default for UltimateOscillator {
75    fn default() -> Self {
76        Self::new(7, 14, 28)
77    }
78}
79
80impl UltimateOscillator {
81    /// The values over `data`, every bar of which has finite inputs.
82    fn compute(&self, data: &[Bar]) -> IndicatorValues {
83        let mut values = IndicatorValues::new();
84        if data.is_empty() {
85            return values;
86        }
87
88        let max_period = self.period1.max(self.period2).max(self.period3);
89        if data.len() < max_period.saturating_add(1) {
90            for _ in 0..data.len() {
91                values.push(IndicatorValue::None);
92            }
93            return values;
94        }
95
96        // Calculate BP (Buying Pressure) and TR (True Range) for each bar
97        let mut bp = vec![0.0; data.len()];
98        let mut tr = vec![0.0; data.len()];
99
100        for i in 1..data.len() {
101            let prev_close = data[i - 1].close;
102            let low_min = data[i].low.min(prev_close);
103            let high_max = data[i].high.max(prev_close);
104
105            bp[i] = data[i].close - low_min;
106            tr[i] = high_max - low_min;
107        }
108
109        // Calculate UO for each bar
110        for i in 0..data.len() {
111            if i < max_period {
112                values.push(IndicatorValue::None);
113                continue;
114            }
115
116            // Calculate sums for each period
117            let bp1: f64 = bp[(i + 1 - self.period1)..=i].iter().sum();
118            let tr1: f64 = tr[(i + 1 - self.period1)..=i].iter().sum();
119
120            let bp2: f64 = bp[(i + 1 - self.period2)..=i].iter().sum();
121            let tr2: f64 = tr[(i + 1 - self.period2)..=i].iter().sum();
122
123            let bp3: f64 = bp[(i + 1 - self.period3)..=i].iter().sum();
124            let tr3: f64 = tr[(i + 1 - self.period3)..=i].iter().sum();
125
126            // Avoid division by zero
127            let avg1 = if tr1 != 0.0 { bp1 / tr1 } else { 0.5 };
128            let avg2 = if tr2 != 0.0 { bp2 / tr2 } else { 0.5 };
129            let avg3 = if tr3 != 0.0 { bp3 / tr3 } else { 0.5 };
130
131            // UO = 100 * ((4 * Avg1) + (2 * Avg2) + Avg3) / 7
132            let uo = 100.0 * ((4.0 * avg1) + (2.0 * avg2) + avg3) / 7.0;
133            values.push(IndicatorValue::Single(uo));
134        }
135
136        values
137    }
138}
139
140impl Indicator for UltimateOscillator {
141    crate::studies::inputs::study_inputs!(self;
142        period1: length("Period1", 1, usize::MAX),
143        period2: length("Period2", 1, usize::MAX),
144        period3: length("Period3", 1, usize::MAX)
145    ; {});
146
147    fn name(&self) -> &str {
148        "UO"
149    }
150
151    fn desc(&self) -> &str {
152        "Ultimate Oscillator - Multi-timeframe momentum indicator"
153    }
154
155    fn calculate(&mut self, data: &[Bar]) {
156        self.update(data, 0);
157    }
158
159    fn update(&mut self, data: &[Bar], from: usize) {
160        let mut values = std::mem::take(&mut self.values);
161        update_window(
162            &mut values,
163            data,
164            from,
165            usable_behind(self.period1.max(self.period2).max(self.period3), |b| {
166                b.high.is_finite() && b.low.is_finite() && b.close.is_finite()
167            }),
168            |data| {
169                over_finite_bars(
170                    data,
171                    |b| b.high.is_finite() && b.low.is_finite() && b.close.is_finite(),
172                    |data| self.compute(data),
173                )
174            },
175        );
176        self.values = values;
177    }
178
179    fn is_incremental(&self) -> bool {
180        true
181    }
182
183    fn values(&self) -> &IndicatorValues {
184        &self.values
185    }
186
187    fn colors(&self) -> Vec<Color32> {
188        vec![self.color]
189    }
190
191    fn set_colors(&mut self, colors: Vec<Color32>) {
192        if !colors.is_empty() {
193            self.color = colors[0];
194        }
195    }
196
197    fn is_overlay(&self) -> bool {
198        false // UO is a separate oscillator pane
199    }
200
201    fn line_count(&self) -> usize {
202        1
203    }
204
205    fn is_visible(&self) -> bool {
206        self.visible
207    }
208
209    fn set_visible(&mut self, visible: bool) {
210        self.visible = visible;
211    }
212
213    fn clone_box(&self) -> Box<dyn Indicator> {
214        Box::new(self.clone())
215    }
216
217    fn line_names(&self) -> Vec<String> {
218        vec![format!(
219            "UO({}, {}, {})",
220            self.period1, self.period2, self.period3
221        )]
222    }
223}