quantwave_core/indicators/
instantaneous_trendline.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 InstantaneousTrendline {
13 wma_price: EhlersWma4,
14 hilbert_detrender: HilbertFIR,
15 hilbert_q1: HilbertFIR,
16 hilbert_ji: HilbertFIR,
17 hilbert_jq: HilbertFIR,
18
19 price_history: VecDeque<f64>,
20 detrender_history: VecDeque<f64>,
21 i1_history: VecDeque<f64>,
22 q1_history: VecDeque<f64>,
23
24 i2_prev: f64,
25 q2_prev: f64,
26 re_prev: f64,
27 im_prev: f64,
28 period_prev: f64,
29 smooth_period_prev: f64,
30
31 itrend_wma: EhlersWma4,
32 _itrend_history: VecDeque<f64>,
33 count: usize,
34}
35
36impl InstantaneousTrendline {
37 pub fn new() -> Self {
38 Self {
39 wma_price: EhlersWma4::new(),
40 hilbert_detrender: HilbertFIR::new(),
41 hilbert_q1: HilbertFIR::new(),
42 hilbert_ji: HilbertFIR::new(),
43 hilbert_jq: HilbertFIR::new(),
44
45 price_history: VecDeque::from(vec![0.0; 50]),
46 detrender_history: VecDeque::from(vec![0.0; 7]),
47 i1_history: VecDeque::from(vec![0.0; 7]),
48 q1_history: VecDeque::from(vec![0.0; 7]),
49
50 i2_prev: 0.0,
51 q2_prev: 0.0,
52 re_prev: 0.0,
53 im_prev: 0.0,
54 period_prev: 6.0,
55 smooth_period_prev: 6.0,
56
57 itrend_wma: EhlersWma4::new(),
58 _itrend_history: VecDeque::from(vec![0.0; 4]),
59 count: 0,
60 }
61 }
62}
63
64impl Default for InstantaneousTrendline {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl Next<f64> for InstantaneousTrendline {
71 type Output = f64;
72
73 fn next(&mut self, price: f64) -> Self::Output {
74 self.count += 1;
75
76 self.price_history.pop_back();
77 self.price_history.push_front(price);
78
79 if self.count < 7 {
80 self.wma_price.next(price);
81 return price;
82 }
83
84 let smooth = self.wma_price.next(price);
85 let detrender = self.hilbert_detrender.next(smooth, self.period_prev);
86
87 self.detrender_history.pop_back();
88 self.detrender_history.push_front(detrender);
89
90 let q1 = self.hilbert_q1.next(detrender, self.period_prev);
91 let i1 = self.detrender_history[3];
92
93 self.i1_history.pop_back();
94 self.i1_history.push_front(i1);
95 self.q1_history.pop_back();
96 self.q1_history.push_front(q1);
97
98 let ji = self.hilbert_ji.next(i1, self.period_prev);
99 let jq = self.hilbert_jq.next(q1, self.period_prev);
100
101 let mut i2 = i1 - jq;
102 let mut q2 = q1 + ji;
103
104 i2 = 0.2 * i2 + 0.8 * self.i2_prev;
106 q2 = 0.2 * q2 + 0.8 * self.q2_prev;
107
108 let mut re = i2 * self.i2_prev + q2 * self.q2_prev;
110 let mut im = i2 * self.q2_prev - q2 * self.i2_prev;
111
112 self.i2_prev = i2;
113 self.q2_prev = q2;
114
115 re = 0.2 * re + 0.8 * self.re_prev;
116 im = 0.2 * im + 0.8 * self.im_prev;
117 self.re_prev = re;
118 self.im_prev = im;
119
120 let mut period = self.period_prev;
121 if im != 0.0 && re != 0.0 {
122 period = 360.0 / (im / re).atan().to_degrees();
123 }
124 if period > 1.5 * self.period_prev {
125 period = 1.5 * self.period_prev;
126 }
127 if period < 0.67 * self.period_prev {
128 period = 0.67 * self.period_prev;
129 }
130 period = period.clamp(6.0, 50.0);
131 period = 0.2 * period + 0.8 * self.period_prev;
132 self.period_prev = period;
133
134 let smooth_period = 0.33 * period + 0.67 * self.smooth_period_prev;
135 self.smooth_period_prev = smooth_period;
136
137 let dc_period = (smooth_period + 0.5) as usize;
139
140 let mut itrend = 0.0;
141 for i in 0..dc_period {
142 if i < self.price_history.len() {
143 itrend += self.price_history[i];
144 }
145 }
146 if dc_period > 0 {
147 itrend /= dc_period as f64;
148 }
149
150 let trendline = self.itrend_wma.next(itrend);
151
152 if self.count < 12 {
153 return price;
154 }
155
156 trendline
157 }
158}
159
160pub const INSTANTANEOUS_TRENDLINE_METADATA: IndicatorMetadata = IndicatorMetadata {
161 name: "Instantaneous Trendline",
162 description: "Removes the dominant cycle to reveal the underlying trend with minimal lag.",
163 usage: "Use as an adaptive trend line that automatically adjusts to the current dominant cycle period, replacing fixed-period moving averages in trend-following systems.",
164 keywords: &["trend", "adaptive", "moving-average", "ehlers", "dsp"],
165 ehlers_summary: "Defined in Rocket Science for Traders (2001), the Instantaneous Trendline is derived from Hilbert Transform phasors and synchronized to the current market cycle. It is computed as a 3-bar weighted average adjusted by the instantaneous period, giving a zero-lag trend estimate.",
166 params: &[],
167 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/ROCKET%20SCIENCE%20FOR%20TRADER.pdf",
168 formula_latex: r#"
169\[
170Trendline = \text{WMA}(\text{SMA}(Price, DCPeriod), 4)
171\]
172"#,
173 gold_standard_file: "instantaneous_trendline.json",
174 category: "Rocket Science",
175};
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use crate::traits::Next;
181 use proptest::prelude::*;
182
183 #[test]
184 fn test_instantaneous_trendline_basic() {
185 let mut it = InstantaneousTrendline::new();
186 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];
187 for p in prices {
188 let res = it.next(p);
189 assert!(!res.is_nan());
190 }
191 }
192
193 proptest! {
194 #[test]
195 fn test_instantaneous_trendline_parity(
196 inputs in prop::collection::vec(1.0..100.0, 50..100),
197 ) {
198 let mut it = InstantaneousTrendline::new();
199 let streaming_results: Vec<f64> = inputs.iter().map(|&x| it.next(x)).collect();
200
201 let mut it_batch = InstantaneousTrendline::new();
203 let batch_results: Vec<f64> = inputs.iter().map(|&x| it_batch.next(x)).collect();
204
205 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
206 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
207 }
208 }
209 }
210}