Skip to main content

quantwave_core/indicators/incremental/
ultosc.rs

1//! Native streaming Ultimate Oscillator — TA-Lib parity (`talib_rs::momentum::ultosc`).
2
3use crate::traits::Next;
4
5/// Ultimate Oscillator — default periods 7, 14, 28.
6#[derive(Debug, Clone)]
7#[allow(non_camel_case_types)]
8pub struct ULTOSC {
9    pub timeperiod1: usize,
10    pub timeperiod2: usize,
11    pub timeperiod3: usize,
12    max_period: usize,
13    prev_close: Option<f64>,
14    bp: Vec<f64>,
15    tr: Vec<f64>,
16    sum_bp1: f64,
17    sum_tr1: f64,
18    sum_bp2: f64,
19    sum_tr2: f64,
20    sum_bp3: f64,
21    sum_tr3: f64,
22    initialized: bool,
23}
24
25impl ULTOSC {
26    pub fn new(timeperiod1: usize, timeperiod2: usize, timeperiod3: usize) -> Self {
27        let max_period = timeperiod1.max(timeperiod2).max(timeperiod3);
28        Self {
29            timeperiod1,
30            timeperiod2,
31            timeperiod3,
32            max_period,
33            prev_close: None,
34            bp: vec![0.0],
35            tr: vec![0.0],
36            sum_bp1: 0.0,
37            sum_tr1: 0.0,
38            sum_bp2: 0.0,
39            sum_tr2: 0.0,
40            sum_bp3: 0.0,
41            sum_tr3: 0.0,
42            initialized: false,
43        }
44    }
45
46    #[inline]
47    fn ultosc_value(&self) -> f64 {
48        let avg1 = if self.sum_tr1 > 0.0 {
49            self.sum_bp1 / self.sum_tr1
50        } else {
51            0.0
52        };
53        let avg2 = if self.sum_tr2 > 0.0 {
54            self.sum_bp2 / self.sum_tr2
55        } else {
56            0.0
57        };
58        let avg3 = if self.sum_tr3 > 0.0 {
59            self.sum_bp3 / self.sum_tr3
60        } else {
61            0.0
62        };
63        100.0 * (4.0 * avg1 + 2.0 * avg2 + avg3) / 7.0
64    }
65
66    fn init_sums(&mut self, i: usize) {
67        let p1 = self.timeperiod1;
68        let p2 = self.timeperiod2;
69        let p3 = self.timeperiod3;
70        self.sum_bp1 = self.bp[(i + 1 - p1)..=i].iter().sum();
71        self.sum_tr1 = self.tr[(i + 1 - p1)..=i].iter().sum();
72        self.sum_bp2 = self.bp[(i + 1 - p2)..=i].iter().sum();
73        self.sum_tr2 = self.tr[(i + 1 - p2)..=i].iter().sum();
74        self.sum_bp3 = self.bp[(i + 1 - p3)..=i].iter().sum();
75        self.sum_tr3 = self.tr[(i + 1 - p3)..=i].iter().sum();
76        self.initialized = true;
77    }
78
79    fn slide_sums(&mut self, i: usize) {
80        let p1 = self.timeperiod1;
81        let p2 = self.timeperiod2;
82        let p3 = self.timeperiod3;
83        self.sum_bp1 += self.bp[i] - self.bp[i - p1];
84        self.sum_tr1 += self.tr[i] - self.tr[i - p1];
85        self.sum_bp2 += self.bp[i] - self.bp[i - p2];
86        self.sum_tr2 += self.tr[i] - self.tr[i - p2];
87        self.sum_bp3 += self.bp[i] - self.bp[i - p3];
88        self.sum_tr3 += self.tr[i] - self.tr[i - p3];
89    }
90}
91
92impl Next<(f64, f64, f64)> for ULTOSC {
93    type Output = f64;
94
95    fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
96        let max_period = self.max_period;
97
98        let Some(prev_close) = self.prev_close else {
99            self.prev_close = Some(close);
100            return f64::NAN;
101        };
102
103        let true_low = low.min(prev_close);
104        let true_high = high.max(prev_close);
105        let bp_val = close - true_low;
106        let tr_val = true_high - true_low;
107
108        self.bp.push(bp_val);
109        self.tr.push(tr_val);
110        self.prev_close = Some(close);
111
112        let i = self.bp.len() - 1;
113        if i < max_period {
114            return f64::NAN;
115        }
116
117        if !self.initialized {
118            self.init_sums(i);
119            return self.ultosc_value();
120        }
121
122        self.slide_sums(i);
123        self.ultosc_value()
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use proptest::prelude::*;
131
132    fn ordered_hlc(
133        h: &[f64],
134        l: &[f64],
135        c: &[f64],
136    ) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
137        let len = h.len().min(l.len()).min(c.len());
138        let mut high = Vec::with_capacity(len);
139        let mut low = Vec::with_capacity(len);
140        let mut close = Vec::with_capacity(len);
141        for i in 0..len {
142            let vh = h[i];
143            let vl = l[i];
144            let vc = c[i];
145            high.push(vh.max(vl).max(vc));
146            low.push(vh.min(vl).min(vc));
147            close.push(vc);
148        }
149        (high, low, close)
150    }
151
152    proptest! {
153        #[test]
154        fn test_ultosc_parity(
155            h in prop::collection::vec(1.0..100.0, 10..100),
156            l in prop::collection::vec(1.0..100.0, 10..100),
157            c in prop::collection::vec(1.0..100.0, 10..100),
158        ) {
159            let (high, low, close) = ordered_hlc(&h, &l, &c);
160            let len = high.len();
161            if len == 0 { return Ok(()); }
162            let p1 = 7;
163            let p2 = 14;
164            let p3 = 28;
165            let mut ult = ULTOSC::new(p1, p2, p3);
166            let streaming: Vec<f64> =
167                (0..len).map(|i| ult.next((high[i], low[i], close[i]))).collect();
168            let batch = talib_rs::momentum::ultosc(&high, &low, &close, p1, p2, p3)
169                .unwrap_or_else(|_| vec![f64::NAN; len]);
170            for (s, b) in streaming.iter().zip(batch.iter()) {
171                if s.is_nan() {
172                    assert!(b.is_nan());
173                } else if !b.is_nan() {
174                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
175                }
176            }
177        }
178    }
179}