Skip to main content

quantwave_core/indicators/
ultimate_bands.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::ultimate_smoother::UltimateSmoother;
3use crate::traits::Next;
4use std::collections::VecDeque;
5
6/// Ultimate Bands
7///
8/// Based on John Ehlers' "Ultimate Channel and Ultimate Bands" (S&C 2024).
9/// Replaces the SMA in Bollinger Bands with UltimateSmoother and calculates
10/// Standard Deviation relative to the smoothed center line.
11#[derive(Debug, Clone)]
12pub struct UltimateBands {
13    smoother: UltimateSmoother,
14    num_sds: f64,
15    length: usize,
16    diff_sq_history: VecDeque<f64>,
17    sum_diff_sq: f64,
18}
19
20impl UltimateBands {
21    pub fn new(length: usize, num_sds: f64) -> Self {
22        Self {
23            smoother: UltimateSmoother::new(length),
24            num_sds,
25            length,
26            diff_sq_history: VecDeque::with_capacity(length),
27            sum_diff_sq: 0.0,
28        }
29    }
30}
31
32impl Next<f64> for UltimateBands {
33    type Output = (f64, f64, f64); // (Upper, Center, Lower)
34
35    fn next(&mut self, input: f64) -> Self::Output {
36        let center = self.smoother.next(input);
37
38        let diff = input - center;
39        let diff_sq = diff * diff;
40
41        self.sum_diff_sq += diff_sq;
42        self.diff_sq_history.push_back(diff_sq);
43
44        if self.diff_sq_history.len() > self.length {
45            if let Some(old) = self.diff_sq_history.pop_front() {
46                self.sum_diff_sq -= old;
47            }
48        }
49
50        let sd = if self.sum_diff_sq > 0.0 {
51            (self.sum_diff_sq / self.diff_sq_history.len() as f64).sqrt()
52        } else {
53            0.0
54        };
55
56        let upper = center + self.num_sds * sd;
57        let lower = center - self.num_sds * sd;
58
59        (upper, center, lower)
60    }
61}
62
63pub const ULTIMATE_BANDS_METADATA: IndicatorMetadata = IndicatorMetadata {
64    name: "Ultimate Bands",
65    description: "A Bollinger-style band using UltimateSmoother for the center line and standard deviation of the price-smooth difference for width.",
66    params: &[
67        ParamDef {
68            name: "length",
69            default: "20",
70            description: "Smoothing and SD period",
71        },
72        ParamDef {
73            name: "num_sds",
74            default: "1.0",
75            description: "Standard Deviation multiplier",
76        },
77    ],
78    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/UltimateChannel.pdf",
79    formula_latex: r#"
80\[
81Smooth = UltimateSmoother(Close, Length)
82\]
83\[
84SD = \sqrt{\frac{1}{n}\sum_{i=0}^{n-1} (Close_{t-i} - Smooth_{t-i})^2}
85\]
86\[
87Upper = Smooth + NumSDs \times SD
88\]
89\[
90Lower = Smooth - NumSDs \times SD
91\]
92"#,
93    gold_standard_file: "ultimate_bands.json",
94    category: "Ehlers DSP",
95};
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::traits::Next;
101    use proptest::prelude::*;
102
103    #[test]
104    fn test_ultimate_bands_basic() {
105        let mut ub = UltimateBands::new(20, 1.0);
106        let inputs = vec![10.0, 11.0, 12.0, 11.0, 10.0];
107        for input in inputs {
108            let (u, c, l) = ub.next(input);
109            assert!(!u.is_nan());
110            assert!(!c.is_nan());
111            assert!(!l.is_nan());
112        }
113    }
114
115    proptest! {
116        #[test]
117        fn test_ultimate_bands_parity(
118            inputs in prop::collection::vec(1.0..100.0, 30..100),
119        ) {
120            let length = 20;
121            let num_sds = 1.0;
122            let mut ub = UltimateBands::new(length, num_sds);
123            let streaming_results: Vec<(f64, f64, f64)> = inputs.iter().map(|&x| ub.next(x)).collect();
124
125            // Reference implementation
126            let mut sm = UltimateSmoother::new(length);
127            let mut diff_sqs = Vec::with_capacity(inputs.len());
128            let mut batch_results = Vec::with_capacity(inputs.len());
129
130            for &input in &inputs {
131                let center = sm.next(input);
132                let diff = input - center;
133                diff_sqs.push(diff * diff);
134
135                let start = if diff_sqs.len() > length { diff_sqs.len() - length } else { 0 };
136                let window = &diff_sqs[start..];
137                let sd = (window.iter().sum::<f64>() / window.len() as f64).sqrt();
138
139                batch_results.push((center + num_sds * sd, center, center - num_sds * sd));
140            }
141
142            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
143                approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-10);
144                approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-10);
145                approx::assert_relative_eq!(s.2, b.2, epsilon = 1e-10);
146            }
147        }
148    }
149}