quantwave_core/indicators/
instantaneous_trendline.rs1use crate::indicators::metadata::IndicatorMetadata;
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5#[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]), 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, 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 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 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 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 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 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 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 let mut i2 = i1 - jq;
114 let mut q2 = q1 + ji;
115
116 i2 = 0.2 * i2 + 0.8 * self.i2_prev;
118 q2 = 0.2 * q2 + 0.8 * self.q2_prev;
119
120 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 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 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 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}