quantwave_core/indicators/
precision_trend.rs1use crate::indicators::high_pass::HighPass;
2use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
3use crate::traits::Next;
4
5#[derive(Debug, Clone)]
12pub struct PrecisionTrendAnalysis {
13 hp1: HighPass,
14 hp2: HighPass,
15 prev_trend: f64,
16 length2: f64,
17 count: usize,
18}
19
20impl PrecisionTrendAnalysis {
21 pub fn new(length1: usize, length2: usize) -> Self {
22 Self {
23 hp1: HighPass::new(length1),
24 hp2: HighPass::new(length2),
25 prev_trend: 0.0,
26 length2: length2 as f64,
27 count: 0,
28 }
29 }
30}
31
32impl Default for PrecisionTrendAnalysis {
33 fn default() -> Self {
34 Self::new(250, 40)
35 }
36}
37
38impl Next<f64> for PrecisionTrendAnalysis {
39 type Output = (f64, f64);
40
41 fn next(&mut self, input: f64) -> Self::Output {
42 self.count += 1;
43 let v_hp1 = self.hp1.next(input);
44 let v_hp2 = self.hp2.next(input);
45 let trend = v_hp1 - v_hp2;
46
47 let roc = (self.length2 / std::f64::consts::TAU) * (trend - self.prev_trend);
48
49 self.prev_trend = trend;
50 (trend, roc)
51 }
52}
53
54pub const PRECISION_TREND_ANALYSIS_METADATA: IndicatorMetadata = IndicatorMetadata {
55 name: "Precision Trend Analysis",
56 description: "Trend identification using the difference between two high-pass filters.",
57 usage: "Use as a high-precision trend indicator that applies DSP filtering to remove cycle noise before measuring trend direction, giving fewer but more reliable trend signals.",
58 keywords: &["trend", "ehlers", "dsp", "filter"],
59 ehlers_summary: "Ehlers Precision Trend analysis applies a roofing-filter style preprocessing to price before computing the trend indicator, removing the cyclical component that causes premature trend reversals in standard indicators. The result is a trend signal that changes state only when the genuine trend direction changes.",
60 params: &[
61 ParamDef {
62 name: "length1",
63 default: "250",
64 description: "First HighPass filter period",
65 },
66 ParamDef {
67 name: "length2",
68 default: "40",
69 description: "Second HighPass filter period",
70 },
71 ],
72 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’%20TIPS%20-%20SEPTEMBER%202024.html",
73 formula_latex: r#"
74\[
75HP1 = HighPass(Price, Length1)
76\]
77\[
78HP2 = HighPass(Price, Length2)
79\]
80\[
81Trend = HP1 - HP2
82\]
83\[
84ROC = \frac{Length2}{6.28} \cdot (Trend - Trend_{t-1})
85\]
86"#,
87 gold_standard_file: "precision_trend.json",
88 category: "Ehlers DSP",
89};
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::traits::Next;
95 use proptest::prelude::*;
96
97 #[test]
98 fn test_precision_trend_basic() {
99 let mut pt = PrecisionTrendAnalysis::new(250, 40);
100 let inputs = vec![10.0; 10];
101 for input in inputs {
102 let (trend, roc) = pt.next(input);
103 assert!(!trend.is_nan());
104 assert!(!roc.is_nan());
105 }
106 }
107
108 proptest! {
109 #[test]
110 fn test_precision_trend_parity(
111 inputs in prop::collection::vec(1.0..100.0, 10..100),
112 ) {
113 let l1 = 250;
114 let l2 = 40;
115 let mut pt = PrecisionTrendAnalysis::new(l1, l2);
116 let streaming_results: Vec<(f64, f64)> = inputs.iter().map(|&x| pt.next(x)).collect();
117
118 let mut hp1 = HighPass::new(l1);
120 let mut hp2 = HighPass::new(l2);
121 let mut prev_trend = 0.0;
122 let mut batch_results = Vec::with_capacity(inputs.len());
123
124 for input in inputs {
125 let v_hp1 = hp1.next(input);
126 let v_hp2 = hp2.next(input);
127 let trend = v_hp1 - v_hp2;
128 let roc = (l2 as f64 / std::f64::consts::TAU) * (trend - prev_trend);
129 prev_trend = trend;
130 batch_results.push((trend, roc));
131 }
132
133 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
134 approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-10);
135 approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-10);
136 }
137 }
138 }
139}