Skip to main content

quantwave_core/indicators/
wavetrend.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::{EMA, SMA};
3use crate::traits::Next;
4
5/// WaveTrend Oscillator
6/// Often referred to as "LazyBear's WaveTrend" on TradingView.
7/// WT1 = EMA(CI, n2)
8/// WT2 = SMA(WT1, n3)
9/// where:
10/// CI = (AP - ESA) / (0.015 * D)
11/// AP = (High + Low + Close) / 3
12/// ESA = EMA(AP, n1)
13/// D = EMA(|AP - ESA|, n1)
14#[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); // (WT1, WT2)
35
36    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        // Feed some dummy data
142        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};