Skip to main content

quantwave_core/indicators/
ultimate_channel.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::ultimate_smoother::UltimateSmoother;
3use crate::traits::Next;
4
5/// Ultimate Channel
6///
7/// Based on John Ehlers' "Ultimate Channel and Ultimate Bands" (S&C 2024).
8/// Replaces the EMA in Keltner Channels with UltimateSmoothers to mitigate lag.
9#[derive(Debug, Clone)]
10pub struct UltimateChannel {
11    center_smoother: UltimateSmoother,
12    str_smoother: UltimateSmoother,
13    num_strs: f64,
14    prev_close: Option<f64>,
15}
16
17impl UltimateChannel {
18    pub fn new(length: usize, str_length: usize, num_strs: f64) -> Self {
19        Self {
20            center_smoother: UltimateSmoother::new(length),
21            str_smoother: UltimateSmoother::new(str_length),
22            num_strs,
23            prev_close: None,
24        }
25    }
26}
27
28impl Next<(f64, f64, f64)> for UltimateChannel {
29    type Output = (f64, f64, f64); // (Upper, Center, Lower)
30
31    fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
32        let th = match self.prev_close {
33            Some(pc) => high.max(pc),
34            None => high,
35        };
36        let tl = match self.prev_close {
37            Some(pc) => low.min(pc),
38            None => low,
39        };
40        self.prev_close = Some(close);
41
42        let str_val = self.str_smoother.next(th - tl);
43        let center = self.center_smoother.next(close);
44
45        let upper = center + self.num_strs * str_val;
46        let lower = center - self.num_strs * str_val;
47
48        (upper, center, lower)
49    }
50}
51
52pub const ULTIMATE_CHANNEL_METADATA: IndicatorMetadata = IndicatorMetadata {
53    name: "Ultimate Channel",
54    description: "A Keltner-style channel using UltimateSmoothers for both the center line and the volatility range to minimize lag.",
55    usage: "Use as a dynamic price channel whose width scales with the current dominant cycle amplitude, providing adaptive support and resistance levels for breakout trading.",
56    keywords: &["channel", "volatility", "ehlers", "adaptive", "breakout"],
57    ehlers_summary: "The Ultimate Channel uses the measured dominant cycle amplitude to set channel width, analogous to Keltner Channels but cycle-aware rather than ATR-based. When price breaks beyond the channel boundary, it signals that cycle amplitude has expanded enough to suggest a genuine directional move.",
58    params: &[
59        ParamDef {
60            name: "length",
61            default: "20",
62            description: "Center line smoothing period",
63        },
64        ParamDef {
65            name: "str_length",
66            default: "20",
67            description: "Smooth True Range (STR) period",
68        },
69        ParamDef {
70            name: "num_strs",
71            default: "1.0",
72            description: "Channel width multiplier",
73        },
74    ],
75    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/UltimateChannel.pdf",
76    formula_latex: r#"
77\[
78TH = \max(High, Close_{t-1})
79\]
80\[
81TL = \min(Low, Close_{t-1})
82\]
83\[
84STR = UltimateSmoother(TH - TL, STRLength)
85\]
86\[
87Center = UltimateSmoother(Close, Length)
88\]
89\[
90Upper = Center + NumSTRs \times STR
91\]
92\[
93Lower = Center - NumSTRs \times STR
94\]
95"#,
96    gold_standard_file: "ultimate_channel.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_ultimate_channel_basic() {
108        let mut uc = UltimateChannel::new(20, 20, 1.0);
109        let inputs = vec![(10.0, 9.0, 9.5), (11.0, 10.0, 10.5), (12.0, 11.0, 11.5)];
110        for input in inputs {
111            let (u, c, l) = uc.next(input);
112            assert!(!u.is_nan());
113            assert!(!c.is_nan());
114            assert!(!l.is_nan());
115            assert!(u >= c);
116            assert!(c >= l);
117        }
118    }
119
120    proptest! {
121        #[test]
122        fn test_ultimate_channel_parity(
123            inputs in prop::collection::vec((10.0..20.0, 5.0..10.0, 7.0..15.0), 30..100),
124        ) {
125            let length = 20;
126            let str_length = 20;
127            let num_strs = 1.0;
128            let mut uc = UltimateChannel::new(length, str_length, num_strs);
129
130            let mut streaming_results = Vec::with_capacity(inputs.len());
131            for &val in &inputs {
132                streaming_results.push(uc.next(val));
133            }
134
135            // Reference implementation
136            let mut center_sm = UltimateSmoother::new(length);
137            let mut str_sm = UltimateSmoother::new(str_length);
138            let mut prev_close = None;
139            let mut batch_results = Vec::with_capacity(inputs.len());
140
141            for &(h, l, c) in &inputs {
142                let th = prev_close.map(|pc: f64| h.max(pc)).unwrap_or(h);
143                let tl = prev_close.map(|pc: f64| l.min(pc)).unwrap_or(l);
144                prev_close = Some(c);
145
146                let str_val = str_sm.next(th - tl);
147                let center = center_sm.next(c);
148                batch_results.push((center + num_strs * str_val, center, center - num_strs * str_val));
149            }
150
151            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
152                approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-10);
153                approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-10);
154                approx::assert_relative_eq!(s.2, b.2, epsilon = 1e-10);
155            }
156        }
157    }
158}