Skip to main content

quantwave_core/indicators/
instantaneous_trendline.rs

1use crate::indicators::metadata::IndicatorMetadata;
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5/// Instantaneous Trendline
6///
7/// Based on John Ehlers' "Rocket Science for Traders" (Chapter 10).
8/// Removes the dominant cycle component to reveal the underlying trend
9/// with minimal lag.
10#[derive(Debug, Clone)]
11pub struct InstantaneousTrendline {
12    price_history: VecDeque<f64>,
13    smooth_history: VecDeque<f64>,
14    detrender_history: VecDeque<f64>,
15    i1_history: VecDeque<f64>,
16    q1_history: VecDeque<f64>,
17    i2_prev: f64,
18    q2_prev: f64,
19    re_prev: f64,
20    im_prev: f64,
21    period_prev: f64,
22    smooth_period_prev: f64,
23    itrend_history: VecDeque<f64>,
24    count: usize,
25}
26
27impl InstantaneousTrendline {
28    pub fn new() -> Self {
29        Self {
30            price_history: VecDeque::from(vec![0.0; 50]), // Enough for DCPeriod
31            smooth_history: VecDeque::from(vec![0.0; 7]),
32            detrender_history: VecDeque::from(vec![0.0; 7]),
33            i1_history: VecDeque::from(vec![0.0; 7]),
34            q1_history: VecDeque::from(vec![0.0; 7]),
35            i2_prev: 0.0,
36            q2_prev: 0.0,
37            re_prev: 0.0,
38            im_prev: 0.0,
39            period_prev: 6.0, // Initial seed
40            smooth_period_prev: 6.0,
41            itrend_history: VecDeque::from(vec![0.0; 4]),
42            count: 0,
43        }
44    }
45}
46
47impl Default for InstantaneousTrendline {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53impl Next<f64> for InstantaneousTrendline {
54    type Output = f64;
55
56    fn next(&mut self, price: f64) -> Self::Output {
57        self.count += 1;
58
59        self.price_history.pop_back();
60        self.price_history.push_front(price);
61
62        if self.count < 7 {
63            return price;
64        }
65
66        // Smooth = (4*Price + 3*Price[1] + 2*Price[2] + Price[3]) / 10;
67        let smooth = (4.0 * self.price_history[0]
68            + 3.0 * self.price_history[1]
69            + 2.0 * self.price_history[2]
70            + self.price_history[3])
71            / 10.0;
72
73        self.smooth_history.pop_back();
74        self.smooth_history.push_front(smooth);
75
76        // Detrender = (.0962*Smooth + .5769*Smooth[2] - .5769*Smooth[4] - .0962*Smooth[6])*(.075*Period[1] + .54);
77        let detrender = (0.0962 * self.smooth_history[0] + 0.5769 * self.smooth_history[2]
78            - 0.5769 * self.smooth_history[4]
79            - 0.0962 * self.smooth_history[6])
80            * (0.075 * self.period_prev + 0.54);
81
82        self.detrender_history.pop_back();
83        self.detrender_history.push_front(detrender);
84
85        // Q1 = (.0962*Detrender + .5769*Detrender[2] - .5769*Detrender[4] - .0962*Detrender[6])*(.075*Period[1] + .54);
86        let q1 = (0.0962 * self.detrender_history[0] + 0.5769 * self.detrender_history[2]
87            - 0.5769 * self.detrender_history[4]
88            - 0.0962 * self.detrender_history[6])
89            * (0.075 * self.period_prev + 0.54);
90
91        // I1 = Detrender[3];
92        let i1 = self.detrender_history[3];
93
94        self.i1_history.pop_back();
95        self.i1_history.push_front(i1);
96        self.q1_history.pop_back();
97        self.q1_history.push_front(q1);
98
99        // jI = (.0962*I1 + .5769*I1[2] - .5769*I1[4] - .0962*I1[6])*(.075*Period[1] + .54);
100        let ji = (0.0962 * self.i1_history[0] + 0.5769 * self.i1_history[2]
101            - 0.5769 * self.i1_history[4]
102            - 0.0962 * self.i1_history[6])
103            * (0.075 * self.period_prev + 0.54);
104
105        // jQ = (.0962*Q1 + .5769*Q1[2] - .5769*Q1[4] - .0962*Q1[6])*(.075*Period[1] + .54);
106        let jq = (0.0962 * self.q1_history[0] + 0.5769 * self.q1_history[2]
107            - 0.5769 * self.q1_history[4]
108            - 0.0962 * self.q1_history[6])
109            * (0.075 * self.period_prev + 0.54);
110
111        // I2 = I1 - jQ;
112        // Q2 = Q1 + jI;
113        let mut i2 = i1 - jq;
114        let mut q2 = q1 + ji;
115
116        // Smooth I and Q components
117        i2 = 0.2 * i2 + 0.8 * self.i2_prev;
118        q2 = 0.2 * q2 + 0.8 * self.q2_prev;
119        
120        // Homodyne Discriminator
121        let mut re = i2 * self.i2_prev + q2 * self.q2_prev;
122        let mut im = i2 * self.q2_prev - q2 * self.i2_prev;
123
124        self.i2_prev = i2;
125        self.q2_prev = q2;
126
127        re = 0.2 * re + 0.8 * self.re_prev;
128        im = 0.2 * im + 0.8 * self.im_prev;
129        self.re_prev = re;
130        self.im_prev = im;
131
132        let mut period = self.period_prev;
133        if im != 0.0 && re != 0.0 {
134            period = 360.0 / (im / re).atan().to_degrees();
135        }
136        if period > 1.5 * self.period_prev {
137            period = 1.5 * self.period_prev;
138        }
139        if period < 0.67 * self.period_prev {
140            period = 0.67 * self.period_prev;
141        }
142        if period < 6.0 {
143            period = 6.0;
144        }
145        if period > 50.0 {
146            period = 50.0;
147        }
148        period = 0.2 * period + 0.8 * self.period_prev;
149        self.period_prev = period;
150
151        let smooth_period = 0.33 * period + 0.67 * self.smooth_period_prev;
152        self.smooth_period_prev = smooth_period;
153
154        // DCPeriod = IntPortion(SmoothPeriod + .5);
155        let dc_period = (smooth_period + 0.5) as usize;
156        
157        let mut itrend = 0.0;
158        for i in 0..dc_period {
159            if i < self.price_history.len() {
160                itrend += self.price_history[i];
161            }
162        }
163        if dc_period > 0 {
164            itrend /= dc_period as f64;
165        }
166
167        self.itrend_history.pop_back();
168        self.itrend_history.push_front(itrend);
169
170        // Trendline = (4*ITrend + 3*ITrend[1] + 2*ITrend[2] + ITrend[3]) / 10;
171        let trendline = (4.0 * self.itrend_history[0]
172            + 3.0 * self.itrend_history[1]
173            + 2.0 * self.itrend_history[2]
174            + self.itrend_history[3])
175            / 10.0;
176
177        if self.count < 12 {
178            return price;
179        }
180        
181        trendline
182    }
183}
184
185pub const INSTANTANEOUS_TRENDLINE_METADATA: IndicatorMetadata = IndicatorMetadata {
186    name: "Instantaneous Trendline",
187    description: "Removes the dominant cycle to reveal the underlying trend with minimal lag.",
188    params: &[],
189    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/ROCKET%20SCIENCE%20FOR%20TRADER.pdf",
190    formula_latex: r#"
191\[
192Trendline = \text{WMA}(\text{SMA}(Price, DCPeriod), 4)
193\]
194"#,
195    gold_standard_file: "instantaneous_trendline.json",
196    category: "Rocket Science",
197};
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::traits::Next;
203    use proptest::prelude::*;
204
205    #[test]
206    fn test_instantaneous_trendline_basic() {
207        let mut it = InstantaneousTrendline::new();
208        let prices = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0];
209        for p in prices {
210            let res = it.next(p);
211            assert!(!res.is_nan());
212        }
213    }
214
215    proptest! {
216        #[test]
217        fn test_instantaneous_trendline_parity(
218            inputs in prop::collection::vec(1.0..100.0, 50..100),
219        ) {
220            let mut it = InstantaneousTrendline::new();
221            let streaming_results: Vec<f64> = inputs.iter().map(|&x| it.next(x)).collect();
222
223            // Reference implementation (batch)
224            let mut it_batch = InstantaneousTrendline::new();
225            let batch_results: Vec<f64> = inputs.iter().map(|&x| it_batch.next(x)).collect();
226
227            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
228                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
229            }
230        }
231    }
232}