1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
43pub struct UltimateOscillator {
44 short: usize,
45 mid: usize,
46 long: usize,
47 longest: usize,
48 prev_close: Option<f64>,
49 window: VecDeque<(f64, f64)>,
51 sum_bp_short: f64,
52 sum_tr_short: f64,
53 sum_bp_mid: f64,
54 sum_tr_mid: f64,
55 sum_bp_long: f64,
56 sum_tr_long: f64,
57 pairs: usize,
58 last: Option<f64>,
59}
60
61impl UltimateOscillator {
62 pub fn new(short: usize, mid: usize, long: usize) -> Result<Self> {
68 if short == 0 || mid == 0 || long == 0 {
69 return Err(Error::PeriodZero);
70 }
71 let longest = short.max(mid).max(long);
72 Ok(Self {
73 short,
74 mid,
75 long,
76 longest,
77 prev_close: None,
78 window: VecDeque::with_capacity(longest + 1),
79 sum_bp_short: 0.0,
80 sum_tr_short: 0.0,
81 sum_bp_mid: 0.0,
82 sum_tr_mid: 0.0,
83 sum_bp_long: 0.0,
84 sum_tr_long: 0.0,
85 pairs: 0,
86 last: None,
87 })
88 }
89
90 pub fn classic() -> Self {
92 Self::new(7, 14, 28).expect("classic Ultimate Oscillator periods are valid")
93 }
94
95 pub const fn periods(&self) -> (usize, usize, usize) {
97 (self.short, self.mid, self.long)
98 }
99
100 pub const fn value(&self) -> Option<f64> {
102 self.last
103 }
104}
105
106impl Indicator for UltimateOscillator {
107 type Input = Candle;
108 type Output = f64;
109
110 fn update(&mut self, candle: Candle) -> Option<f64> {
111 let Some(prev_close) = self.prev_close else {
112 self.prev_close = Some(candle.close);
114 return None;
115 };
116 self.prev_close = Some(candle.close);
117
118 let true_low = candle.low.min(prev_close);
119 let bp = candle.close - true_low;
120 let tr = candle.high.max(prev_close) - true_low;
121
122 self.window.push_back((bp, tr));
123 let n = self.window.len();
124 self.sum_bp_short += bp;
125 self.sum_tr_short += tr;
126 self.sum_bp_mid += bp;
127 self.sum_tr_mid += tr;
128 self.sum_bp_long += bp;
129 self.sum_tr_long += tr;
130 if n > self.short {
131 let (b, t) = self.window[n - 1 - self.short];
132 self.sum_bp_short -= b;
133 self.sum_tr_short -= t;
134 }
135 if n > self.mid {
136 let (b, t) = self.window[n - 1 - self.mid];
137 self.sum_bp_mid -= b;
138 self.sum_tr_mid -= t;
139 }
140 if n > self.long {
141 let (b, t) = self.window[n - 1 - self.long];
142 self.sum_bp_long -= b;
143 self.sum_tr_long -= t;
144 }
145 if self.window.len() > self.longest {
146 self.window.pop_front();
147 }
148
149 self.pairs += 1;
150 if self.pairs < self.longest {
151 return None;
152 }
153
154 let avg = |bp_sum: f64, tr_sum: f64| {
155 if tr_sum == 0.0 {
156 0.5
158 } else {
159 bp_sum / tr_sum
160 }
161 };
162 let avg_short = avg(self.sum_bp_short, self.sum_tr_short);
163 let avg_mid = avg(self.sum_bp_mid, self.sum_tr_mid);
164 let avg_long = avg(self.sum_bp_long, self.sum_tr_long);
165 let uo = 100.0 * (4.0 * avg_short + 2.0 * avg_mid + avg_long) / 7.0;
166 self.last = Some(uo);
167 Some(uo)
168 }
169
170 fn reset(&mut self) {
171 self.prev_close = None;
172 self.window.clear();
173 self.sum_bp_short = 0.0;
174 self.sum_tr_short = 0.0;
175 self.sum_bp_mid = 0.0;
176 self.sum_tr_mid = 0.0;
177 self.sum_bp_long = 0.0;
178 self.sum_tr_long = 0.0;
179 self.pairs = 0;
180 self.last = None;
181 }
182
183 #[inline]
184 fn warmup_period(&self) -> usize {
185 self.longest + 1
188 }
189
190 #[inline]
191 fn is_ready(&self) -> bool {
192 self.last.is_some()
193 }
194
195 #[inline]
196 fn name(&self) -> &'static str {
197 "UltimateOscillator"
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use crate::traits::BatchExt;
205 use approx::assert_relative_eq;
206
207 fn flat(price: f64, ts: i64) -> Candle {
209 Candle::new(price, price, price, price, 1.0, ts).unwrap()
210 }
211
212 #[test]
213 fn new_rejects_zero_period() {
214 assert!(matches!(
215 UltimateOscillator::new(0, 14, 28),
216 Err(Error::PeriodZero)
217 ));
218 assert!(matches!(
219 UltimateOscillator::new(7, 0, 28),
220 Err(Error::PeriodZero)
221 ));
222 assert!(matches!(
223 UltimateOscillator::new(7, 14, 0),
224 Err(Error::PeriodZero)
225 ));
226 }
227
228 #[test]
232 fn accessors_and_metadata() {
233 let mut uo = UltimateOscillator::new(7, 14, 28).unwrap();
234 assert_eq!(uo.periods(), (7, 14, 28));
235 assert_eq!(uo.name(), "UltimateOscillator");
236 assert_eq!(uo.value(), None);
237 let warmup = i64::try_from(uo.warmup_period()).unwrap();
238 let candles: Vec<Candle> = (0..warmup)
239 .map(|i| {
240 let p = 100.0 + (i as f64 * 0.3).sin() * 5.0;
241 Candle::new(p, p + 1.0, p - 1.0, p, 1.0, i).unwrap()
242 })
243 .collect();
244 for c in &candles {
245 uo.update(*c);
246 }
247 assert!(uo.value().is_some());
248 }
249
250 #[test]
251 fn first_emission_at_warmup_period() {
252 let mut uo = UltimateOscillator::new(2, 3, 5).unwrap();
253 assert_eq!(uo.warmup_period(), 6);
254 let candles: Vec<Candle> = (0..20).map(|i| flat(100.0 + i as f64, i)).collect();
255 let out = uo.batch(&candles);
256 for v in out.iter().take(5) {
257 assert!(v.is_none());
258 }
259 assert!(out[5].is_some());
260 }
261
262 #[test]
263 fn pure_uptrend_saturates_at_100() {
264 let mut uo = UltimateOscillator::new(2, 3, 5).unwrap();
267 let candles: Vec<Candle> = (0..30).map(|i| flat(100.0 + i as f64, i)).collect();
268 for v in uo.batch(&candles).into_iter().flatten() {
269 assert_relative_eq!(v, 100.0, epsilon = 1e-9);
270 }
271 }
272
273 #[test]
274 fn pure_downtrend_saturates_at_0() {
275 let mut uo = UltimateOscillator::new(2, 3, 5).unwrap();
277 let candles: Vec<Candle> = (0..30).map(|i| flat(100.0 - i as f64, i)).collect();
278 for v in uo.batch(&candles).into_iter().flatten() {
279 assert_relative_eq!(v, 0.0, epsilon = 1e-9);
280 }
281 }
282
283 #[test]
284 fn flat_market_reads_50() {
285 let mut uo = UltimateOscillator::new(2, 3, 5).unwrap();
287 let candles: Vec<Candle> = (0..30).map(|i| flat(100.0, i)).collect();
288 for v in uo.batch(&candles).into_iter().flatten() {
289 assert_relative_eq!(v, 50.0, epsilon = 1e-9);
290 }
291 }
292
293 #[test]
294 fn output_stays_within_0_100() {
295 let mut uo = UltimateOscillator::classic();
296 let candles: Vec<Candle> = (0..200)
297 .map(|i| {
298 let mid = 100.0 + (i as f64 * 0.2).sin() * 12.0;
299 Candle::new(mid, mid + 3.0, mid - 3.0, mid + 1.0, 10.0, i).unwrap()
300 })
301 .collect();
302 for v in uo.batch(&candles).into_iter().flatten() {
303 assert!((0.0..=100.0).contains(&v), "UO out of range: {v}");
304 }
305 }
306
307 #[test]
308 fn reset_clears_state() {
309 let mut uo = UltimateOscillator::new(2, 3, 5).unwrap();
310 let candles: Vec<Candle> = (0..20).map(|i| flat(100.0 + i as f64, i)).collect();
311 uo.batch(&candles);
312 assert!(uo.is_ready());
313 uo.reset();
314 assert!(!uo.is_ready());
315 assert_eq!(uo.update(candles[0]), None);
316 }
317
318 #[test]
319 fn batch_equals_streaming() {
320 let candles: Vec<Candle> = (0..120)
321 .map(|i| {
322 let mid = 100.0 + (i as f64 * 0.3).sin() * 10.0;
323 Candle::new(mid, mid + 2.0, mid - 2.0, mid + 0.5, 10.0, i).unwrap()
324 })
325 .collect();
326 let batch = UltimateOscillator::classic().batch(&candles);
327 let mut b = UltimateOscillator::classic();
328 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
329 assert_eq!(batch, streamed);
330 }
331}