quantwave_core/indicators/
homodyne_discriminator.rs1use crate::indicators::metadata::IndicatorMetadata;
2use crate::indicators::hilbert_transform::{HilbertFIR, EhlersWma4};
3use crate::traits::Next;
4use std::collections::VecDeque;
5
6#[derive(Debug, Clone)]
12pub struct HomodyneDiscriminator {
13 wma_price: EhlersWma4,
14 hilbert_detrender: HilbertFIR,
15 hilbert_q1: HilbertFIR,
16 hilbert_ji: HilbertFIR,
17 hilbert_jq: HilbertFIR,
18
19 detrender_history: VecDeque<f64>,
20 i1_history: VecDeque<f64>,
21 q1_history: VecDeque<f64>,
22
23 i2_prev: f64,
24 q2_prev: f64,
25 re_prev: f64,
26 im_prev: f64,
27 period_prev: f64,
28 count: usize,
29}
30
31impl HomodyneDiscriminator {
32 pub fn new() -> Self {
33 Self {
34 wma_price: EhlersWma4::new(),
35 hilbert_detrender: HilbertFIR::new(),
36 hilbert_q1: HilbertFIR::new(),
37 hilbert_ji: HilbertFIR::new(),
38 hilbert_jq: HilbertFIR::new(),
39
40 detrender_history: VecDeque::from(vec![0.0; 7]),
41 i1_history: VecDeque::from(vec![0.0; 7]),
42 q1_history: VecDeque::from(vec![0.0; 7]),
43
44 i2_prev: 0.0,
45 q2_prev: 0.0,
46 re_prev: 0.0,
47 im_prev: 0.0,
48 period_prev: 6.0,
49 count: 0,
50 }
51 }
52}
53
54impl Default for HomodyneDiscriminator {
55 fn default() -> Self {
56 Self::new()
57 }
58}
59
60impl Next<f64> for HomodyneDiscriminator {
61 type Output = f64;
62
63 fn next(&mut self, price: f64) -> Self::Output {
64 self.count += 1;
65
66 if self.count < 7 {
67 self.wma_price.next(price);
68 return 0.0;
69 }
70
71 let smooth = self.wma_price.next(price);
72 let detrender = self.hilbert_detrender.next(smooth, self.period_prev);
73
74 self.detrender_history.pop_back();
75 self.detrender_history.push_front(detrender);
76
77 let q1 = self.hilbert_q1.next(detrender, self.period_prev);
78 let i1 = self.detrender_history[3];
79
80 self.i1_history.pop_back();
81 self.i1_history.push_front(i1);
82 self.q1_history.pop_back();
83 self.q1_history.push_front(q1);
84
85 let ji = self.hilbert_ji.next(i1, self.period_prev);
86 let jq = self.hilbert_jq.next(q1, self.period_prev);
87
88 let mut i2 = i1 - jq;
89 let mut q2 = q1 + ji;
90
91 i2 = 0.2 * i2 + 0.8 * self.i2_prev;
93 q2 = 0.2 * q2 + 0.8 * self.q2_prev;
94
95 let mut re = i2 * self.i2_prev + q2 * self.q2_prev;
97 let mut im = i2 * self.q2_prev - q2 * self.i2_prev;
98
99 self.i2_prev = i2;
100 self.q2_prev = q2;
101
102 re = 0.2 * re + 0.8 * self.re_prev;
103 im = 0.2 * im + 0.8 * self.im_prev;
104 self.re_prev = re;
105 self.im_prev = im;
106
107 let mut period = self.period_prev;
108 if im != 0.0 && re != 0.0 {
109 period = 360.0 / (im / re).atan().to_degrees();
110 }
111 if period > 1.5 * self.period_prev {
112 period = 1.5 * self.period_prev;
113 }
114 if period < 0.67 * self.period_prev {
115 period = 0.67 * self.period_prev;
116 }
117 period = period.clamp(6.0, 50.0);
118 period = 0.2 * period + 0.8 * self.period_prev;
119 self.period_prev = period;
120
121 period
122 }
123}
124
125pub const HOMODYNE_DISCRIMINATOR_METADATA: IndicatorMetadata = IndicatorMetadata {
126 name: "Homodyne Discriminator",
127 description: "Estimates the dominant cycle period using a homodyne approach.",
128 usage: "Use to measure the instantaneous dominant cycle period from price data. Feed its output into adaptive indicators as the dynamic period parameter.",
129 keywords: &["cycle", "dominant-cycle", "ehlers", "dsp", "spectral"],
130 ehlers_summary: "Described in Rocket Science for Traders (2001), the Homodyne Discriminator borrows from radio engineering to measure instantaneous frequency by multiplying the analytic signal by its one-bar-delayed conjugate, giving cycle period without DFT latency.",
131 params: &[],
132 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/ROCKET%20SCIENCE%20FOR%20TRADER.pdf",
133 formula_latex: r#"
134\[
135\text{Period} = \frac{360}{\text{atan}(Im / Re)}
136\]
137"#,
138 gold_standard_file: "homodyne_discriminator.json",
139 category: "Rocket Science",
140};
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::traits::Next;
146 use proptest::prelude::*;
147
148 #[test]
149 fn test_homodyne_discriminator_basic() {
150 let mut hd = HomodyneDiscriminator::new();
151 for i in 0..100 {
152 let val = hd.next((2.0 * std::f64::consts::PI * i as f64 / 20.0).sin());
154 if i > 50 {
155 assert!(val > 10.0 && val < 30.0);
156 }
157 }
158 }
159
160 proptest! {
161 #[test]
162 fn test_homodyne_discriminator_parity(
163 inputs in prop::collection::vec(1.0..100.0, 50..100),
164 ) {
165 let mut hd = HomodyneDiscriminator::new();
166 let streaming_results: Vec<f64> = inputs.iter().map(|&x| hd.next(x)).collect();
167
168 let mut hd_batch = HomodyneDiscriminator::new();
169 let batch_results: Vec<f64> = inputs.iter().map(|&x| hd_batch.next(x)).collect();
170
171 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
172 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
173 }
174 }
175 }
176}