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    usage: "Use to measure the relative strength of the current market move normalized to the dominant cycle amplitude, giving a volatility-adjusted momentum reading.",
74    keywords: &["oscillator", "strength", "ehlers", "adaptive", "momentum"],
75    ehlers_summary: "The Ultimate Strength Index measures directional momentum as a fraction of the total cycle amplitude. By normalizing momentum to the RMS energy of the dominant cycle, it produces a consistent 0-100 reading that is comparable across different instruments and volatility regimes.",
76    params: &[ParamDef {
77        name: "length",
78        default: "14",
79        description: "UltimateSmoother period",
80    }],
81    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS%E2%80%99%20TIPS%20-%20NOVEMBER%202024.html",
82    formula_latex: r#"
83\[
84\text{SU} = \max(0, \text{Close} - \text{Close}_{t-1})
85\]
86\[
87\text{SD} = \max(0, \text{Close}_{t-1} - \text{Close})
88\]
89\[
90\text{USU} = UltimateSmoother(SMA(\text{SU}, 4), Length)
91\]
92\[
93\text{USD} = UltimateSmoother(SMA(\text{SD}, 4), Length)
94\]
95\[
96\text{USI} = \frac{\text{USU} - \text{USD}}{\text{USU} + \text{USD}}
97\]
98"#,
99    gold_standard_file: "usi.json",
100    category: "Ehlers DSP",
101};
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use crate::traits::Next;
107    use proptest::prelude::*;
108
109    #[test]
110    fn test_usi_basic() {
111        let mut usi = USI::new(14);
112        let inputs = vec![100.0, 101.0, 102.0, 101.0, 100.0, 99.0, 98.0, 99.0, 100.0];
113        for input in inputs {
114            let res = usi.next(input);
115            assert!(!res.is_nan());
116            assert!(res >= -1.0 && res <= 1.0);
117        }
118    }
119
120    proptest! {
121        #[test]
122        fn test_usi_parity(
123            inputs in prop::collection::vec(10.0..110.0, 50..100),
124        ) {
125            let length = 14;
126            let mut usi = USI::new(length);
127            let streaming_results: Vec<f64> = inputs.iter().map(|&x| usi.next(x)).collect();
128
129            // Reference implementation
130            let mut su_sma = SMA::new(4);
131            let mut sd_sma = SMA::new(4);
132            let mut usu = UltimateSmoother::new(length);
133            let mut usd = UltimateSmoother::new(length);
134            let mut last_val = 0.0;
135            let mut batch_results = Vec::with_capacity(inputs.len());
136
137            for i in 0..inputs.len() {
138                let (su, sd) = if i == 0 {
139                    (0.0, 0.0)
140                } else {
141                    let diff = inputs[i] - inputs[i-1];
142                    if diff > 0.0 { (diff, 0.0) } else { (0.0, -diff) }
143                };
144
145                let s_val = su_sma.next(su);
146                let d_val = sd_sma.next(sd);
147
148                let u_smooth = usu.next(s_val);
149                let d_smooth = usd.next(d_val);
150
151                let denom = u_smooth + d_smooth;
152                if denom.abs() > 1e-10 && u_smooth.abs() > 1e-12 && d_smooth.abs() > 1e-12 {
153                    last_val = (u_smooth - d_smooth) / denom;
154                }
155                batch_results.push(last_val);
156            }
157
158            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
159                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
160            }
161        }
162    }
163}