quantwave_core/indicators/
ultimate_bands.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::ultimate_smoother::UltimateSmoother;
3use crate::traits::Next;
4use crate::utils::RingBuffer as VecDeque;
5
6#[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); 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 && let Some(old) = self.diff_sq_history.pop_front()
46 {
47 self.sum_diff_sq -= old;
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 usage: "Use as volatility bands that automatically widen during high-energy cycle phases and narrow during quiet phases. Better than fixed-multiple ATR bands in strongly cyclical markets.",
67 keywords: &["bands", "volatility", "ehlers", "dsp", "adaptive"],
68 ehlers_summary: "Ehlers Ultimate Bands compute upper and lower price envelopes using the RMS amplitude of the dominant cycle rather than a fixed ATR multiple. This makes the bands proportional to the current cycle energy, expanding when the market is actively cycling and contracting when it enters a low-energy consolidation.",
69 params: &[
70 ParamDef {
71 name: "length",
72 default: "20",
73 description: "Smoothing and SD period",
74 },
75 ParamDef {
76 name: "num_sds",
77 default: "1.0",
78 description: "Standard Deviation multiplier",
79 },
80 ],
81 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/UltimateChannel.pdf",
82 formula_latex: r#"
83\[
84Smooth = UltimateSmoother(Close, Length)
85\]
86\[
87SD = \sqrt{\frac{1}{n}\sum_{i=0}^{n-1} (Close_{t-i} - Smooth_{t-i})^2}
88\]
89\[
90Upper = Smooth + NumSDs \times SD
91\]
92\[
93Lower = Smooth - NumSDs \times SD
94\]
95"#,
96 gold_standard_file: "ultimate_bands.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_bands_basic() {
108 let mut ub = UltimateBands::new(20, 1.0);
109 let inputs = vec![10.0, 11.0, 12.0, 11.0, 10.0];
110 for input in inputs {
111 let (u, c, l) = ub.next(input);
112 assert!(!u.is_nan());
113 assert!(!c.is_nan());
114 assert!(!l.is_nan());
115 }
116 }
117
118 proptest! {
119 #[test]
120 fn test_ultimate_bands_parity(
121 inputs in prop::collection::vec(1.0..100.0, 30..100),
122 ) {
123 let length = 20;
124 let num_sds = 1.0;
125 let mut ub = UltimateBands::new(length, num_sds);
126 let streaming_results: Vec<(f64, f64, f64)> = inputs.iter().map(|&x| ub.next(x)).collect();
127
128 let mut sm = UltimateSmoother::new(length);
130 let mut diff_sqs = Vec::with_capacity(inputs.len());
131 let mut batch_results = Vec::with_capacity(inputs.len());
132
133 for &input in &inputs {
134 let center = sm.next(input);
135 let diff = input - center;
136 diff_sqs.push(diff * diff);
137
138 let start = if diff_sqs.len() > length { diff_sqs.len() - length } else { 0 };
139 let window = &diff_sqs[start..];
140 let sd = (window.iter().sum::<f64>() / window.len() as f64).sqrt();
141
142 batch_results.push((center + num_sds * sd, center, center - num_sds * sd));
143 }
144
145 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
146 approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-10);
147 approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-10);
148 approx::assert_relative_eq!(s.2, b.2, epsilon = 1e-10);
149 }
150 }
151 }
152}