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 params: &[
56 ParamDef {
57 name: "length",
58 default: "20",
59 description: "Center line smoothing period",
60 },
61 ParamDef {
62 name: "str_length",
63 default: "20",
64 description: "Smooth True Range (STR) period",
65 },
66 ParamDef {
67 name: "num_strs",
68 default: "1.0",
69 description: "Channel width multiplier",
70 },
71 ],
72 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/UltimateChannel.pdf",
73 formula_latex: r#"
74\[
75TH = \max(High, Close_{t-1})
76\]
77\[
78TL = \min(Low, Close_{t-1})
79\]
80\[
81STR = UltimateSmoother(TH - TL, STRLength)
82\]
83\[
84Center = UltimateSmoother(Close, Length)
85\]
86\[
87Upper = Center + NumSTRs \times STR
88\]
89\[
90Lower = Center - NumSTRs \times STR
91\]
92"#,
93 gold_standard_file: "ultimate_channel.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_channel_basic() {
105 let mut uc = UltimateChannel::new(20, 20, 1.0);
106 let inputs = vec![(10.0, 9.0, 9.5), (11.0, 10.0, 10.5), (12.0, 11.0, 11.5)];
107 for input in inputs {
108 let (u, c, l) = uc.next(input);
109 assert!(!u.is_nan());
110 assert!(!c.is_nan());
111 assert!(!l.is_nan());
112 assert!(u >= c);
113 assert!(c >= l);
114 }
115 }
116
117 proptest! {
118 #[test]
119 fn test_ultimate_channel_parity(
120 inputs in prop::collection::vec((10.0..20.0, 5.0..10.0, 7.0..15.0), 30..100),
121 ) {
122 let length = 20;
123 let str_length = 20;
124 let num_strs = 1.0;
125 let mut uc = UltimateChannel::new(length, str_length, num_strs);
126
127 let mut streaming_results = Vec::with_capacity(inputs.len());
128 for &val in &inputs {
129 streaming_results.push(uc.next(val));
130 }
131
132 let mut center_sm = UltimateSmoother::new(length);
134 let mut str_sm = UltimateSmoother::new(str_length);
135 let mut prev_close = None;
136 let mut batch_results = Vec::with_capacity(inputs.len());
137
138 for &(h, l, c) in &inputs {
139 let th = prev_close.map(|pc: f64| h.max(pc)).unwrap_or(h);
140 let tl = prev_close.map(|pc: f64| l.min(pc)).unwrap_or(l);
141 prev_close = Some(c);
142
143 let str_val = str_sm.next(th - tl);
144 let center = center_sm.next(c);
145 batch_results.push((center + num_strs * str_val, center, center - num_strs * str_val));
146 }
147
148 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
149 approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-10);
150 approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-10);
151 approx::assert_relative_eq!(s.2, b.2, epsilon = 1e-10);
152 }
153 }
154 }
155}