Skip to main content

quantwave_core/indicators/
phasor.rs

1use crate::indicators::metadata::IndicatorMetadata;
2use crate::indicators::hilbert_transform::{HilbertFIR, EhlersWma4};
3use crate::traits::Next;
4use std::collections::VecDeque;
5
6/// Phasor Indicator
7///
8/// Based on John Ehlers' "Rocket Science for Traders" (Chapter 9).
9/// Decomposes the signal into its In-Phase (I) and Quadrature (Q) components.
10/// Returns (InPhase, Quadrature).
11#[derive(Debug, Clone)]
12pub struct Phasor {
13    wma_price: EhlersWma4,
14    hilbert_detrender: HilbertFIR,
15    hilbert_q1: HilbertFIR,
16    
17    detrender_history: VecDeque<f64>,
18    period_prev: f64,
19    count: usize,
20}
21
22impl Phasor {
23    pub fn new() -> Self {
24        Self {
25            wma_price: EhlersWma4::new(),
26            hilbert_detrender: HilbertFIR::new(),
27            hilbert_q1: HilbertFIR::new(),
28            
29            detrender_history: VecDeque::from(vec![0.0; 7]),
30            period_prev: 6.0,
31            count: 0,
32        }
33    }
34
35    /// Update with a specific period for the Hilbert FIR
36    pub fn next_with_period(&mut self, price: f64, period: f64) -> (f64, f64) {
37        self.count += 1;
38        self.period_prev = period.clamp(6.0, 50.0);
39
40        if self.count < 7 {
41            self.wma_price.next(price);
42            return (0.0, 0.0);
43        }
44
45        let smooth = self.wma_price.next(price);
46        let detrender = self.hilbert_detrender.next(smooth, self.period_prev);
47        
48        self.detrender_history.pop_back();
49        self.detrender_history.push_front(detrender);
50
51        let q1 = self.hilbert_q1.next(detrender, self.period_prev);
52        let i1 = self.detrender_history[3];
53
54        (i1, q1)
55    }
56}
57
58impl Default for Phasor {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64impl Next<f64> for Phasor {
65    type Output = (f64, f64);
66
67    fn next(&mut self, price: f64) -> Self::Output {
68        self.next_with_period(price, self.period_prev)
69    }
70}
71
72pub const PHASOR_METADATA: IndicatorMetadata = IndicatorMetadata {
73    name: "Phasor",
74    description: "Extracts In-Phase (I) and Quadrature (Q) components using a Hilbert Transform.",
75    usage: "Use to measure the instantaneous phase and amplitude of the dominant market cycle. Phase crossings of key angles (90, 180 degrees) provide precise cycle turn timing signals.",
76    keywords: &["cycle", "phase", "ehlers", "dsp", "dominant-cycle"],
77    ehlers_summary: "Ehlers borrows the concept of a phasor from electrical engineering to represent the amplitude and phase of a market cycle as a rotating vector. In Rocket Science for Traders (2001) he shows how measuring the instantaneous phasor angle gives more precise cycle timing than zero-crossing methods.",
78    params: &[],
79    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/ROCKET%20SCIENCE%20FOR%20TRADER.pdf",
80    formula_latex: r#"
81\[
82I = \text{Detrender}_{t-3}
83\]
84\[
85Q = \text{HilbertFIR}(\text{Detrender}, \text{Period})
86\]
87"#,
88    gold_standard_file: "phasor.json",
89    category: "Rocket Science",
90};
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::traits::Next;
96    use proptest::prelude::*;
97
98    #[test]
99    fn test_phasor_basic() {
100        let mut p = Phasor::new();
101        let prices = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0];
102        for price in prices {
103            let (i, q) = p.next(price);
104            assert!(!i.is_nan());
105            assert!(!q.is_nan());
106        }
107    }
108
109    proptest! {
110        #[test]
111        fn test_phasor_parity(
112            inputs in prop::collection::vec(1.0..100.0, 50..100),
113        ) {
114            let mut p = Phasor::new();
115            let streaming_results: Vec<(f64, f64)> = inputs.iter().map(|&x| p.next(x)).collect();
116
117            let mut p_batch = Phasor::new();
118            let batch_results: Vec<(f64, f64)> = inputs.iter().map(|&x| p_batch.next(x)).collect();
119
120            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
121                approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-10);
122                approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-10);
123            }
124        }
125    }
126}