quantwave_core/indicators/
amfm.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4use std::f64::consts::PI;
5
6#[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#[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 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 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}