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