Skip to main content

quantwave_core/indicators/
amfm.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4use std::f64::consts::PI;
5
6/// AM Detector (Volatility)
7/// 
8/// Based on John Ehlers' "A Technical Description of Market Data for Traders".
9/// It recovers market volatility by detecting the envelope of the amplitude-modulated 
10/// whitened price spectrum.
11#[derive(Debug, Clone)]
12pub struct AMDetector {
13    highest_len: usize,
14    avg_len: usize,
15    deriv_history: VecDeque<f64>,
16    envelope_history: VecDeque<f64>,
17    sum_envelope: f64,
18}
19
20impl AMDetector {
21    pub fn new(highest_len: usize, avg_len: usize) -> Self {
22        Self {
23            highest_len,
24            avg_len,
25            deriv_history: VecDeque::with_capacity(highest_len),
26            envelope_history: VecDeque::with_capacity(avg_len),
27            sum_envelope: 0.0,
28        }
29    }
30}
31
32impl Next<(f64, f64)> for AMDetector {
33    type Output = f64;
34
35    fn next(&mut self, (close, open): (f64, f64)) -> Self::Output {
36        let deriv = (close - open).abs();
37        self.deriv_history.push_front(deriv);
38        if self.deriv_history.len() > self.highest_len {
39            self.deriv_history.pop_back();
40        }
41
42        let envelope = self.deriv_history.iter().fold(f64::MIN, |a, &b| a.max(b));
43        
44        self.envelope_history.push_back(envelope);
45        self.sum_envelope += envelope;
46        if self.envelope_history.len() > self.avg_len {
47            if let Some(old) = self.envelope_history.pop_front() {
48                self.sum_envelope -= old;
49            }
50        }
51
52        self.sum_envelope / self.envelope_history.len() as f64
53    }
54}
55
56/// FM Demodulator (Timing)
57/// 
58/// Based on John Ehlers' "A Technical Description of Market Data for Traders".
59/// It extracts market timing information by demodulating the frequency-modulated 
60/// price spectrum using a hard limiter and a SuperSmoother filter.
61#[derive(Debug, Clone)]
62pub struct FMDemodulator {
63    _period: usize,
64    c1: f64,
65    c2: f64,
66    c3: f64,
67    hl_prev: f64,
68    ss_history: [f64; 2],
69    count: usize,
70}
71
72impl FMDemodulator {
73    pub fn new(period: usize) -> Self {
74        let a1 = (-1.414 * PI / period as f64).exp();
75        let c2 = 2.0 * a1 * (1.414 * PI / period as f64).cos();
76        let c3 = -a1 * a1;
77        let c1 = 1.0 - c2 - c3;
78        
79        Self {
80            _period: period,
81            c1,
82            c2,
83            c3,
84            hl_prev: 0.0,
85            ss_history: [0.0; 2],
86            count: 0,
87        }
88    }
89}
90
91impl Next<(f64, f64)> for FMDemodulator {
92    type Output = f64;
93
94    fn next(&mut self, (close, open): (f64, f64)) -> Self::Output {
95        self.count += 1;
96        let deriv = close - open;
97        let mut hl = 10.0 * deriv;
98        if hl > 1.0 { hl = 1.0; }
99        if hl < -1.0 { hl = -1.0; }
100
101        let ss = if self.count < 3 {
102            deriv
103        } else {
104            self.c1 * (hl + self.hl_prev) / 2.0
105                + self.c2 * self.ss_history[0]
106                + self.c3 * self.ss_history[1]
107        };
108
109        self.ss_history[1] = self.ss_history[0];
110        self.ss_history[0] = ss;
111        self.hl_prev = hl;
112        
113        ss
114    }
115}
116
117pub const AM_DETECTOR_METADATA: IndicatorMetadata = IndicatorMetadata {
118    name: "AM Detector",
119    description: "Recovers market volatility from the amplitude-modulated whitened price spectrum.",
120    params: &[
121        ParamDef { name: "highest_len", default: "4", description: "Envelope lookback length" },
122        ParamDef { name: "avg_len", default: "8", description: "Smoothing length" },
123    ],
124    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/AMFM.pdf",
125    formula_latex: r#"
126\[
127Deriv = |Close - Open|, Envel = \max(Deriv, 4), Volatil = \text{Avg}(Envel, 8)
128\]
129"#,
130    gold_standard_file: "am_detector.json",
131    category: "Ehlers DSP",
132};
133
134pub const FM_DEMODULATOR_METADATA: IndicatorMetadata = IndicatorMetadata {
135    name: "FM Demodulator",
136    description: "Extracts market timing information by demodulating the frequency-modulated price spectrum.",
137    params: &[
138        ParamDef { name: "period", default: "30", description: "SuperSmoother period" },
139    ],
140    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/AMFM.pdf",
141    formula_latex: r#"
142\[
143Deriv = Close - Open, HL = \text{Clip}(10 \times Deriv, -1, 1)
144\]
145\[
146SS = c_1 \frac{HL + HL_{t-1}}{2} + c_2 SS_{t-1} + c_3 SS_{t-2}
147\]
148"#,
149    gold_standard_file: "fm_demodulator.json",
150    category: "Ehlers DSP",
151};
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use crate::traits::Next;
157    use proptest::prelude::*;
158
159    #[test]
160    fn test_am_detector_basic() {
161        let mut am = AMDetector::new(4, 8);
162        let inputs = vec![(10.0, 9.0), (11.0, 10.0), (12.0, 11.0)];
163        for input in inputs {
164            let res = am.next(input);
165            assert!(res >= 0.0);
166        }
167    }
168
169    #[test]
170    fn test_fm_demodulator_basic() {
171        let mut fm = FMDemodulator::new(30);
172        let inputs = vec![(10.0, 9.0), (11.0, 10.0), (12.0, 11.0)];
173        for input in inputs {
174            let res = fm.next(input);
175            assert!(!res.is_nan());
176        }
177    }
178
179    proptest! {
180        #[test]
181        fn test_am_detector_parity(
182            closes in prop::collection::vec(1.0..100.0, 50..100),
183            opens in prop::collection::vec(1.0..100.0, 50..100),
184        ) {
185            let h_len = 4;
186            let a_len = 8;
187            let mut am = AMDetector::new(h_len, a_len);
188            let inputs: Vec<(f64, f64)> = closes.iter().zip(opens.iter()).map(|(&c, &o)| (c, o)).collect();
189            let streaming_results: Vec<f64> = inputs.iter().map(|&x| am.next(x)).collect();
190            
191            // Batch implementation
192            let mut batch_results = Vec::with_capacity(inputs.len());
193            let mut envelope_hist = VecDeque::new();
194            let mut sum_env = 0.0;
195            
196            for i in 0..inputs.len() {
197                let start = if i >= h_len { i + 1 - h_len } else { 0 };
198                let mut max_deriv = f64::MIN;
199                for j in start..=i {
200                    let deriv = (inputs[j].0 - inputs[j].1).abs();
201                    if deriv > max_deriv { max_deriv = deriv; }
202                }
203                
204                envelope_hist.push_back(max_deriv);
205                sum_env += max_deriv;
206                if envelope_hist.len() > a_len {
207                    if let Some(old) = envelope_hist.pop_front() {
208                        sum_env -= old;
209                    }
210                }
211                batch_results.push(sum_env / envelope_hist.len() as f64);
212            }
213            
214            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
215                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
216            }
217        }
218
219        #[test]
220        fn test_fm_demodulator_parity(
221            closes in prop::collection::vec(1.0..100.0, 50..100),
222            opens in prop::collection::vec(1.0..100.0, 50..100),
223        ) {
224            let period = 30;
225            let mut fm = FMDemodulator::new(period);
226            let inputs: Vec<(f64, f64)> = closes.iter().zip(opens.iter()).map(|(&c, &o)| (c, o)).collect();
227            let streaming_results: Vec<f64> = inputs.iter().map(|&x| fm.next(x)).collect();
228            
229            // Batch implementation
230            let mut batch_results = Vec::with_capacity(inputs.len());
231            let a1 = (-1.414 * PI / period as f64).exp();
232            let c2 = 2.0 * a1 * (1.414 * PI / period as f64).cos();
233            let c3 = -a1 * a1;
234            let c1 = 1.0 - c2 - c3;
235            
236            let mut hl_prev = 0.0;
237            let mut ss_hist = [0.0; 2];
238            
239            for (i, &input) in inputs.iter().enumerate() {
240                let deriv = input.0 - input.1;
241                let mut hl = 10.0 * deriv;
242                if hl > 1.0 { hl = 1.0; }
243                if hl < -1.0 { hl = -1.0; }
244                
245                let res = if i + 1 < 3 {
246                    deriv
247                } else {
248                    c1 * (hl + hl_prev) / 2.0 + c2 * ss_hist[0] + c3 * ss_hist[1]
249                };
250                
251                ss_hist[1] = ss_hist[0];
252                ss_hist[0] = res;
253                hl_prev = hl;
254                batch_results.push(res);
255            }
256            
257            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
258                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
259            }
260        }
261    }
262}