Skip to main content

quantwave_core/indicators/
usi.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::SMA;
3use crate::indicators::ultimate_smoother::UltimateSmoother;
4use crate::traits::Next;
5
6/// Ultimate Strength Index (USI)
7///
8/// Based on John Ehlers' article "Ultimate Strength Index (USI)" (TASC November 2024).
9/// An enhanced version of the RSI with significantly reduced lag and smoother response.
10/// It applies the UltimateSmoother to a 4-bar simple moving average of up and down moves.
11#[derive(Debug, Clone)]
12pub struct USI {
13    prev_close: Option<f64>,
14    su_sma: SMA,
15    sd_sma: SMA,
16    usu: UltimateSmoother,
17    usd: UltimateSmoother,
18    last_val: f64,
19}
20
21impl USI {
22    pub fn new(length: usize) -> Self {
23        Self {
24            prev_close: None,
25            su_sma: SMA::new(4),
26            sd_sma: SMA::new(4),
27            usu: UltimateSmoother::new(length),
28            usd: UltimateSmoother::new(length),
29            last_val: 0.0,
30        }
31    }
32}
33
34impl Next<f64> for USI {
35    type Output = f64;
36
37    fn next(&mut self, input: f64) -> Self::Output {
38        let (su, sd) = match self.prev_close {
39            Some(prev) => {
40                if input > prev {
41                    (input - prev, 0.0)
42                } else if input < prev {
43                    (0.0, prev - input)
44                } else {
45                    (0.0, 0.0)
46                }
47            }
48            None => (0.0, 0.0),
49        };
50        self.prev_close = Some(input);
51
52        let su_sma_val = self.su_sma.next(su);
53        let sd_sma_val = self.sd_sma.next(sd);
54
55        let usu_val = self.usu.next(su_sma_val);
56        let usd_val = self.usd.next(sd_sma_val);
57
58        let denom = usu_val + usd_val;
59        // Using a small epsilon to avoid jitter and divide-by-zero
60        // The original code uses a 0.01 threshold which might be too large for small-priced assets
61        // We'll use 1e-10 and only update if conditions are met, otherwise carry forward last value
62        if denom.abs() > 1e-10 && usu_val.abs() > 1e-12 && usd_val.abs() > 1e-12 {
63            self.last_val = (usu_val - usd_val) / denom;
64        }
65
66        self.last_val
67    }
68}
69
70pub const USI_METADATA: IndicatorMetadata = IndicatorMetadata {
71    name: "Ultimate Strength Index",
72    description: "A lag-reduced version of the RSI using UltimateSmoother on smoothed up/down components.",
73    params: &[ParamDef {
74        name: "length",
75        default: "14",
76        description: "UltimateSmoother period",
77    }],
78    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS%E2%80%99%20TIPS%20-%20NOVEMBER%202024.html",
79    formula_latex: r#"
80\[
81\text{SU} = \max(0, \text{Close} - \text{Close}_{t-1})
82\]
83\[
84\text{SD} = \max(0, \text{Close}_{t-1} - \text{Close})
85\]
86\[
87\text{USU} = UltimateSmoother(SMA(\text{SU}, 4), Length)
88\]
89\[
90\text{USD} = UltimateSmoother(SMA(\text{SD}, 4), Length)
91\]
92\[
93\text{USI} = \frac{\text{USU} - \text{USD}}{\text{USU} + \text{USD}}
94\]
95"#,
96    gold_standard_file: "usi.json",
97    category: "Ehlers DSP",
98};
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::traits::Next;
104    use proptest::prelude::*;
105
106    #[test]
107    fn test_usi_basic() {
108        let mut usi = USI::new(14);
109        let inputs = vec![100.0, 101.0, 102.0, 101.0, 100.0, 99.0, 98.0, 99.0, 100.0];
110        for input in inputs {
111            let res = usi.next(input);
112            assert!(!res.is_nan());
113            assert!(res >= -1.0 && res <= 1.0);
114        }
115    }
116
117    proptest! {
118        #[test]
119        fn test_usi_parity(
120            inputs in prop::collection::vec(10.0..110.0, 50..100),
121        ) {
122            let length = 14;
123            let mut usi = USI::new(length);
124            let streaming_results: Vec<f64> = inputs.iter().map(|&x| usi.next(x)).collect();
125
126            // Reference implementation
127            let mut su_sma = SMA::new(4);
128            let mut sd_sma = SMA::new(4);
129            let mut usu = UltimateSmoother::new(length);
130            let mut usd = UltimateSmoother::new(length);
131            let mut last_val = 0.0;
132            let mut batch_results = Vec::with_capacity(inputs.len());
133
134            for i in 0..inputs.len() {
135                let (su, sd) = if i == 0 {
136                    (0.0, 0.0)
137                } else {
138                    let diff = inputs[i] - inputs[i-1];
139                    if diff > 0.0 { (diff, 0.0) } else { (0.0, -diff) }
140                };
141
142                let s_val = su_sma.next(su);
143                let d_val = sd_sma.next(sd);
144
145                let u_smooth = usu.next(s_val);
146                let d_smooth = usd.next(d_val);
147
148                let denom = u_smooth + d_smooth;
149                if denom.abs() > 1e-10 && u_smooth.abs() > 1e-12 && d_smooth.abs() > 1e-12 {
150                    last_val = (u_smooth - d_smooth) / denom;
151                }
152                batch_results.push(last_val);
153            }
154
155            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
156                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
157            }
158        }
159    }
160}