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 fn warmup_period(&self) -> usize {
184 self.longest + 1
187 }
188
189 fn is_ready(&self) -> bool {
190 self.last.is_some()
191 }
192
193 fn name(&self) -> &'static str {
194 "UltimateOscillator"
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use crate::traits::BatchExt;
202 use approx::assert_relative_eq;
203
204 fn flat(price: f64, ts: i64) -> Candle {
206 Candle::new(price, price, price, price, 1.0, ts).unwrap()
207 }
208
209 #[test]
210 fn new_rejects_zero_period() {
211 assert!(matches!(
212 UltimateOscillator::new(0, 14, 28),
213 Err(Error::PeriodZero)
214 ));
215 assert!(matches!(
216 UltimateOscillator::new(7, 0, 28),
217 Err(Error::PeriodZero)
218 ));
219 assert!(matches!(
220 UltimateOscillator::new(7, 14, 0),
221 Err(Error::PeriodZero)
222 ));
223 }
224
225 #[test]
229 fn accessors_and_metadata() {
230 let mut uo = UltimateOscillator::new(7, 14, 28).unwrap();
231 assert_eq!(uo.periods(), (7, 14, 28));
232 assert_eq!(uo.name(), "UltimateOscillator");
233 assert_eq!(uo.value(), None);
234 let warmup = i64::try_from(uo.warmup_period()).unwrap();
235 let candles: Vec<Candle> = (0..warmup)
236 .map(|i| {
237 let p = 100.0 + (i as f64 * 0.3).sin() * 5.0;
238 Candle::new(p, p + 1.0, p - 1.0, p, 1.0, i).unwrap()
239 })
240 .collect();
241 for c in &candles {
242 uo.update(*c);
243 }
244 assert!(uo.value().is_some());
245 }
246
247 #[test]
248 fn first_emission_at_warmup_period() {
249 let mut uo = UltimateOscillator::new(2, 3, 5).unwrap();
250 assert_eq!(uo.warmup_period(), 6);
251 let candles: Vec<Candle> = (0..20).map(|i| flat(100.0 + i as f64, i)).collect();
252 let out = uo.batch(&candles);
253 for v in out.iter().take(5) {
254 assert!(v.is_none());
255 }
256 assert!(out[5].is_some());
257 }
258
259 #[test]
260 fn pure_uptrend_saturates_at_100() {
261 let mut uo = UltimateOscillator::new(2, 3, 5).unwrap();
264 let candles: Vec<Candle> = (0..30).map(|i| flat(100.0 + i as f64, i)).collect();
265 for v in uo.batch(&candles).into_iter().flatten() {
266 assert_relative_eq!(v, 100.0, epsilon = 1e-9);
267 }
268 }
269
270 #[test]
271 fn pure_downtrend_saturates_at_0() {
272 let mut uo = UltimateOscillator::new(2, 3, 5).unwrap();
274 let candles: Vec<Candle> = (0..30).map(|i| flat(100.0 - i as f64, i)).collect();
275 for v in uo.batch(&candles).into_iter().flatten() {
276 assert_relative_eq!(v, 0.0, epsilon = 1e-9);
277 }
278 }
279
280 #[test]
281 fn flat_market_reads_50() {
282 let mut uo = UltimateOscillator::new(2, 3, 5).unwrap();
284 let candles: Vec<Candle> = (0..30).map(|i| flat(100.0, i)).collect();
285 for v in uo.batch(&candles).into_iter().flatten() {
286 assert_relative_eq!(v, 50.0, epsilon = 1e-9);
287 }
288 }
289
290 #[test]
291 fn output_stays_within_0_100() {
292 let mut uo = UltimateOscillator::classic();
293 let candles: Vec<Candle> = (0..200)
294 .map(|i| {
295 let mid = 100.0 + (i as f64 * 0.2).sin() * 12.0;
296 Candle::new(mid, mid + 3.0, mid - 3.0, mid + 1.0, 10.0, i).unwrap()
297 })
298 .collect();
299 for v in uo.batch(&candles).into_iter().flatten() {
300 assert!((0.0..=100.0).contains(&v), "UO out of range: {v}");
301 }
302 }
303
304 #[test]
305 fn reset_clears_state() {
306 let mut uo = UltimateOscillator::new(2, 3, 5).unwrap();
307 let candles: Vec<Candle> = (0..20).map(|i| flat(100.0 + i as f64, i)).collect();
308 uo.batch(&candles);
309 assert!(uo.is_ready());
310 uo.reset();
311 assert!(!uo.is_ready());
312 assert_eq!(uo.update(candles[0]), None);
313 }
314
315 #[test]
316 fn batch_equals_streaming() {
317 let candles: Vec<Candle> = (0..120)
318 .map(|i| {
319 let mid = 100.0 + (i as f64 * 0.3).sin() * 10.0;
320 Candle::new(mid, mid + 2.0, mid - 2.0, mid + 0.5, 10.0, i).unwrap()
321 })
322 .collect();
323 let batch = UltimateOscillator::classic().batch(&candles);
324 let mut b = UltimateOscillator::classic();
325 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
326 assert_eq!(batch, streamed);
327 }
328}