quantwave_core/indicators/
pairs_rotation.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::indicators::high_pass::HighPass;
4use crate::indicators::super_smoother::SuperSmoother;
5
6#[derive(Debug, Clone)]
13pub struct PairsRotation {
14 hp1: HighPass,
15 ss1: SuperSmoother,
16 ms1: f64,
17 hp2: HighPass,
18 ss2: SuperSmoother,
19 ms2: f64,
20 count: usize,
21}
22
23impl PairsRotation {
24 pub fn new(hp_len: usize, lp_len: usize) -> Self {
25 Self {
26 hp1: HighPass::new(hp_len),
27 ss1: SuperSmoother::new(lp_len),
28 ms1: 0.0,
29 hp2: HighPass::new(hp_len),
30 ss2: SuperSmoother::new(lp_len),
31 ms2: 0.0,
32 count: 0,
33 }
34 }
35}
36
37impl Default for PairsRotation {
38 fn default() -> Self {
39 Self::new(125, 20)
40 }
41}
42
43impl Next<(f64, f64)> for PairsRotation {
44 type Output = (f64, f64); fn next(&mut self, input: (f64, f64)) -> Self::Output {
47 self.count += 1;
48 let (p1, p2) = input;
49
50 let filt1 = self.ss1.next(self.hp1.next(p1));
51 let filt2 = self.ss2.next(self.hp2.next(p2));
52
53 let alpha = 0.0242;
55 if self.count == 1 {
56 self.ms1 = filt1 * filt1;
57 self.ms2 = filt2 * filt2;
58 } else {
59 self.ms1 = alpha * filt1 * filt1 + (1.0 - alpha) * self.ms1;
60 self.ms2 = alpha * filt2 * filt2 + (1.0 - alpha) * self.ms2;
61 }
62
63 let norm1 = if self.ms1 > 0.0 {
64 filt1 / self.ms1.sqrt()
65 } else {
66 0.0
67 };
68
69 let norm2 = if self.ms2 > 0.0 {
70 filt2 / self.ms2.sqrt()
71 } else {
72 0.0
73 };
74
75 (norm1, norm2)
76 }
77}
78
79pub const PAIRS_ROTATION_METADATA: IndicatorMetadata = IndicatorMetadata {
80 name: "PairsRotation",
81 description: "Relative rotation of two securities using normalized roofing filters.",
82 params: &[
83 ParamDef {
84 name: "hp_len",
85 default: "125",
86 description: "HighPass filter length",
87 },
88 ParamDef {
89 name: "lp_len",
90 default: "20",
91 description: "LowPass (SuperSmoother) length",
92 },
93 ],
94 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/PAIRS%20ROTATION.pdf",
95 formula_latex: r#"
96\[
97Filt = SuperSmoother(HighPass(Price, HPLen), LPLen)
98\]
99\[
100MS = 0.0242 \cdot Filt^2 + 0.9758 \cdot MS_{t-1}
101\]
102\[
103Normalized = \frac{Filt}{\sqrt{MS}}
104\]
105"#,
106 gold_standard_file: "pairs_rotation.json",
107 category: "Ehlers DSP",
108};
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::traits::Next;
114 use proptest::prelude::*;
115
116 #[test]
117 fn test_pairs_rotation_basic() {
118 let mut pr = PairsRotation::new(125, 20);
119 for i in 0..100 {
120 let (n1, n2) = pr.next((100.0 + i as f64, 100.0 - i as f64));
121 assert!(!n1.is_nan());
122 assert!(!n2.is_nan());
123 }
124 }
125
126 proptest! {
127 #[test]
128 fn test_pairs_rotation_parity(
129 inputs1 in prop::collection::vec(1.0..100.0, 100..200),
130 inputs2 in prop::collection::vec(1.0..100.0, 100..200),
131 ) {
132 let hp_len = 125;
133 let lp_len = 20;
134 let mut pr = PairsRotation::new(hp_len, lp_len);
135
136 let min_len = inputs1.len().min(inputs2.len());
137 let inputs: Vec<(f64, f64)> = inputs1[..min_len].iter().cloned().zip(inputs2[..min_len].iter().cloned()).collect();
138 let streaming_results: Vec<(f64, f64)> = inputs.iter().map(|&x| pr.next(x)).collect();
139
140 let mut batch_results = Vec::with_capacity(min_len);
142 let mut hp1 = HighPass::new(hp_len);
143 let mut ss1 = SuperSmoother::new(lp_len);
144 let mut hp2 = HighPass::new(hp_len);
145 let mut ss2 = SuperSmoother::new(lp_len);
146 let mut ms1 = 0.0;
147 let mut ms2 = 0.0;
148 let alpha = 0.0242;
149
150 for (i, &(p1, p2)) in inputs.iter().enumerate() {
151 let f1 = ss1.next(hp1.next(p1));
152 let f2 = ss2.next(hp2.next(p2));
153
154 if i == 0 {
155 ms1 = f1 * f1;
156 ms2 = f2 * f2;
157 } else {
158 ms1 = alpha * f1 * f1 + (1.0 - alpha) * ms1;
159 ms2 = alpha * f2 * f2 + (1.0 - alpha) * ms2;
160 }
161
162 let n1 = if ms1 > 0.0 { f1 / ms1.sqrt() } else { 0.0 };
163 let n2 = if ms2 > 0.0 { f2 / ms2.sqrt() } else { 0.0 };
164 batch_results.push((n1, n2));
165 }
166
167 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
168 approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-10);
169 approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-10);
170 }
171 }
172 }
173}