quantwave_core/indicators/
wavetrend.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::{EMA, SMA};
3use crate::traits::Next;
4
5#[derive(Debug, Clone)]
15pub struct WaveTrend {
16 esa_ema: EMA,
17 d_ema: EMA,
18 wt1_ema: EMA,
19 wt2_sma: SMA,
20}
21
22impl WaveTrend {
23 pub fn new(n1: usize, n2: usize, n3: usize) -> Self {
24 Self {
25 esa_ema: EMA::new(n1),
26 d_ema: EMA::new(n1),
27 wt1_ema: EMA::new(n2),
28 wt2_sma: SMA::new(n3),
29 }
30 }
31}
32
33impl Next<(f64, f64, f64)> for WaveTrend {
34 type Output = (f64, f64); fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
37 let ap = (high + low + close) / 3.0;
38 let esa = self.esa_ema.next(ap);
39 let d_raw = (ap - esa).abs();
40 let d = self.d_ema.next(d_raw);
41
42 let ci = if d != 0.0 {
43 (ap - esa) / (0.015 * d)
44 } else {
45 0.0
46 };
47
48 let wt1 = self.wt1_ema.next(ci);
49 let wt2 = self.wt2_sma.next(wt1);
50
51 (wt1, wt2)
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use proptest::prelude::*;
59 use serde::Deserialize;
60 use std::fs;
61 use std::path::Path;
62
63 #[derive(Debug, Deserialize)]
64 struct WaveTrendCase {
65 high: Vec<f64>,
66 low: Vec<f64>,
67 close: Vec<f64>,
68 expected_wt1: Vec<f64>,
69 expected_wt2: Vec<f64>,
70 }
71
72 #[test]
73 fn test_wavetrend_gold_standard() {
74 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
75 let manifest_path = Path::new(&manifest_dir);
76 let path = manifest_path.join("tests/gold_standard/wavetrend_10_21_4.json");
77 let path = if path.exists() {
78 path
79 } else {
80 manifest_path
81 .parent()
82 .unwrap()
83 .join("tests/gold_standard/wavetrend_10_21_4.json")
84 };
85 let content = fs::read_to_string(path).unwrap();
86 let case: WaveTrendCase = serde_json::from_str(&content).unwrap();
87
88 let mut wt = WaveTrend::new(10, 21, 4);
89 for i in 0..case.high.len() {
90 let (wt1, wt2) = wt.next((case.high[i], case.low[i], case.close[i]));
91 approx::assert_relative_eq!(wt1, case.expected_wt1[i], epsilon = 1e-6);
92 approx::assert_relative_eq!(wt2, case.expected_wt2[i], epsilon = 1e-6);
93 }
94 }
95
96 fn wavetrend_batch(
97 data: Vec<(f64, f64, f64)>,
98 n1: usize,
99 n2: usize,
100 n3: usize,
101 ) -> Vec<(f64, f64)> {
102 let mut wt = WaveTrend::new(n1, n2, n3);
103 data.into_iter().map(|x| wt.next(x)).collect()
104 }
105
106 proptest! {
107 #[test]
108 fn test_wavetrend_parity(input in prop::collection::vec((0.0..100.0, 0.0..100.0, 0.0..100.0), 1..100)) {
109 let mut adj_input = Vec::with_capacity(input.len());
110 for (h, l, c) in input {
111 let h_f: f64 = h;
112 let l_f: f64 = l;
113 let c_f: f64 = c;
114 let high = h_f.max(l_f).max(c_f);
115 let low = l_f.min(h_f).min(c_f);
116 adj_input.push((high, low, c_f));
117 }
118
119 let n1 = 10;
120 let n2 = 21;
121 let n3 = 4;
122 let mut wt = WaveTrend::new(n1, n2, n3);
123 let mut streaming_results = Vec::with_capacity(adj_input.len());
124 for &val in &adj_input {
125 streaming_results.push(wt.next(val));
126 }
127
128 let batch_results = wavetrend_batch(adj_input, n1, n2, n3);
129
130 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
131 approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-6);
132 approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-6);
133 }
134 }
135 }
136
137 #[test]
138 fn test_wavetrend_basic() {
139 let mut wt = WaveTrend::new(10, 21, 4);
140
141 for i in 0..50 {
143 let val = 100.0 + (i as f64).sin() * 5.0;
144 let (wt1, wt2) = wt.next((val + 1.0, val - 1.0, val));
145 if i >= 10 {
146 assert!(!wt1.is_nan());
147 assert!(!wt2.is_nan());
148 }
149 }
150 }
151}
152
153pub const WAVETREND_METADATA: IndicatorMetadata = IndicatorMetadata {
154 name: "WaveTrend Oscillator",
155 description: "WaveTrend is an oscillator that helps identify overbought and oversold conditions.",
156 usage: "Use as a momentum oscillator to identify overbought and oversold conditions. WaveTrend crossovers at extreme levels provide high-probability mean-reversion entry signals.",
157 keywords: &["oscillator", "momentum", "overbought", "oversold", "ehlers"],
158 ehlers_summary: "WaveTrend (popularized as LazyBear WaveTrend on TradingView) computes a channel index by normalizing price deviation from an EMA by the smoothed absolute deviation. A second EMA of this index produces the signal line. Extreme values (±60) with WT1-WT2 crossovers are the classic trade trigger.",
159 params: &[
160 ParamDef {
161 name: "n1",
162 default: "10",
163 description: "Channel Length",
164 },
165 ParamDef {
166 name: "n2",
167 default: "21",
168 description: "Average Length",
169 },
170 ],
171 formula_source: "https://www.tradingview.com/script/2KE8wTuF-Indicator-WaveTrend-Oscillator-WT/",
172 formula_latex: r#"
173\[
174WT_1 = EMA(ESA, n_2)
175\]
176"#,
177 gold_standard_file: "wavetrend.json",
178 category: "Ehlers DSP",
179};