quantwave_core/indicators/
usi.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::SMA;
3use crate::indicators::ultimate_smoother::UltimateSmoother;
4use crate::traits::Next;
5
6#[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 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 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}