Skip to main content

quantwave_core/indicators/
overlap.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2talib_1_in_1_out!(DEMA, talib_rs::overlap::dema, timeperiod: usize);
3impl From<usize> for DEMA {
4    fn from(p: usize) -> Self {
5        Self::new(p)
6    }
7}
8talib_1_in_1_out!(TRIMA, talib_rs::overlap::trima, timeperiod: usize);
9impl From<usize> for TRIMA {
10    fn from(p: usize) -> Self {
11        Self::new(p)
12    }
13}
14talib_1_in_1_out!(KAMA, talib_rs::overlap::kama, timeperiod: usize);
15impl From<usize> for KAMA {
16    fn from(p: usize) -> Self {
17        Self::new(p)
18    }
19}
20talib_1_in_1_out!(T3, talib_rs::overlap::t3, timeperiod: usize, v_factor: f64);
21talib_1_in_2_out!(MAMA, talib_rs::overlap::mama, fastlimit: f64, slowlimit: f64);
22talib_1_in_3_out!(BBANDS, talib_rs::overlap::bbands, timeperiod: usize, nbdevup: f64, nbdevdn: f64, matype: talib_rs::MaType);
23talib_2_in_1_out!(SAR, talib_rs::overlap::sar, acceleration: f64, maximum: f64);
24talib_2_in_1_out!(SAREXT, talib_rs::overlap::sar_ext, startvalue: f64, offsetonreverse: f64, accelerationinitlong: f64, accelerationlong: f64, accelerationmaxlong: f64, accelerationinitshort: f64, accelerationshort: f64, accelerationmaxshort: f64);
25talib_1_in_1_out!(MIDPOINT, talib_rs::overlap::midpoint, timeperiod: usize);
26impl From<usize> for MIDPOINT {
27    fn from(p: usize) -> Self {
28        Self::new(p)
29    }
30}
31talib_2_in_1_out!(MIDPRICE, talib_rs::overlap::midprice, timeperiod: usize);
32impl From<usize> for MIDPRICE {
33    fn from(p: usize) -> Self {
34        Self::new(p)
35    }
36}
37talib_2_in_1_out!(MAVP, talib_rs::overlap::mavp, minperiod: usize, maxperiod: usize, matype: talib_rs::MaType);
38talib_1_in_1_out!(HT_TRENDLINE, talib_rs::overlap::ht_trendline);
39impl Default for HT_TRENDLINE {
40    fn default() -> Self {
41        Self::new()
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48    use crate::traits::Next;
49    use proptest::prelude::*;
50
51    proptest! {
52        #[test]
53        fn test_dema_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
54            let period = 10;
55            let mut dema = DEMA::new(period);
56            let streaming_results: Vec<f64> = input.iter().map(|&x| dema.next(x)).collect();
57            let batch_results = talib_rs::overlap::dema(&input, period).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
58
59            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
60                if s.is_nan() {
61                    assert!(b.is_nan());
62                } else {
63                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
64                }
65            }
66        }
67
68        #[test]
69        fn test_trima_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
70            let period = 10;
71            let mut trima = TRIMA::new(period);
72            let streaming_results: Vec<f64> = input.iter().map(|&x| trima.next(x)).collect();
73            let batch_results = talib_rs::overlap::trima(&input, period).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
74
75            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
76                if s.is_nan() {
77                    assert!(b.is_nan());
78                } else {
79                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
80                }
81            }
82        }
83
84        #[test]
85        fn test_kama_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
86            let period = 10;
87            let mut kama = KAMA::new(period);
88            let streaming_results: Vec<f64> = input.iter().map(|&x| kama.next(x)).collect();
89            let batch_results = talib_rs::overlap::kama(&input, period).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
90
91            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
92                if s.is_nan() {
93                    assert!(b.is_nan());
94                } else {
95                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
96                }
97            }
98        }
99
100        #[test]
101        fn test_t3_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
102            let period = 10;
103            let v_factor = 0.7;
104            let mut t3 = T3::new(period, v_factor);
105            let streaming_results: Vec<f64> = input.iter().map(|&x| t3.next(x)).collect();
106            let batch_results = talib_rs::overlap::t3(&input, period, v_factor).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
107
108            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
109                if s.is_nan() {
110                    assert!(b.is_nan());
111                } else {
112                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
113                }
114            }
115        }
116
117        #[test]
118        fn test_bbands_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
119            let period = 10;
120            let nbdevup = 2.0;
121            let nbdevdn = 2.0;
122            let matype = talib_rs::MaType::Sma;
123            let mut bbands = BBANDS::new(period, nbdevup, nbdevdn, matype);
124            let streaming_results: Vec<(f64, f64, f64)> = input.iter().map(|&x| bbands.next(x)).collect();
125            let (b_upper, b_middle, b_lower) = talib_rs::overlap::bbands(&input, period, nbdevup, nbdevdn, matype).unwrap_or_else(|_| {
126                (vec![f64::NAN; input.len()], vec![f64::NAN; input.len()], vec![f64::NAN; input.len()])
127            });
128
129            for (i, (s_upper, s_middle, s_lower)) in streaming_results.into_iter().enumerate() {
130                if s_upper.is_nan() {
131                    assert!(b_upper[i].is_nan());
132                } else {
133                    approx::assert_relative_eq!(s_upper, b_upper[i], epsilon = 1e-6);
134                }
135                if s_middle.is_nan() {
136                    assert!(b_middle[i].is_nan());
137                } else {
138                    approx::assert_relative_eq!(s_middle, b_middle[i], epsilon = 1e-6);
139                }
140                if s_lower.is_nan() {
141                    assert!(b_lower[i].is_nan());
142                } else {
143                    approx::assert_relative_eq!(s_lower, b_lower[i], epsilon = 1e-6);
144                }
145            }
146        }
147
148        #[test]
149        fn test_sar_parity(
150            h in prop::collection::vec(10.0..100.0, 1..100),
151            l in prop::collection::vec(10.0..100.0, 1..100)
152        ) {
153            let len = h.len().min(l.len());
154            if len == 0 { return Ok(()); }
155            let mut high = Vec::with_capacity(len);
156            let mut low = Vec::with_capacity(len);
157            for i in 0..len {
158                let v_h: f64 = h[i];
159                let v_l: f64 = l[i];
160                high.push(v_h.max(v_l));
161                low.push(v_h.min(v_l));
162            }
163
164            let accel = 0.02;
165            let max = 0.2;
166            let mut sar = SAR::new(accel, max);
167            let streaming_results: Vec<f64> = (0..len).map(|i| sar.next((high[i], low[i]))).collect();
168            let batch_results = talib_rs::overlap::sar(&high, &low, accel, max).unwrap_or_else(|_| vec![f64::NAN; len]);
169
170            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
171                if s.is_nan() {
172                    assert!(b.is_nan());
173                } else {
174                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
175                }
176            }
177        }
178    }
179}
180
181pub const DEMA_METADATA: IndicatorMetadata = IndicatorMetadata {
182    name: "Double Exponential Moving Average (DEMA)",
183    description: "A fast-acting moving average that reduces lag by using two exponential moving averages.",
184    usage: "Use as a replacement for EMA when faster signal generation is required without excessive noise. DEMA reacts more quickly to price changes than a standard EMA.",
185    keywords: &["moving-average", "smoothing", "lag-reduction", "classic"],
186    ehlers_summary: "Developed by Patrick Mulloy in 1994, DEMA provides a less-laggy alternative to traditional moving averages. It is calculated by taking a single EMA and then subtracting it from a double EMA of the same period. This effectively cancels out some of the lag inherent in the EMA calculation. — StockCharts ChartSchool",
187    params: &[ParamDef { name: "timeperiod", default: "30", description: "Smoothing period" }],
188    formula_source: "https://www.investopedia.com/terms/d/double-exponential-moving-average.asp",
189    formula_latex: r#"
190\[
191DEMA = 2 \times EMA - EMA(EMA)
192\]
193"#,
194    gold_standard_file: "dema.json",
195    category: "Classic",
196};
197
198pub const TRIMA_METADATA: IndicatorMetadata = IndicatorMetadata {
199    name: "Triangular Moving Average (TRIMA)",
200    description: "A double-smoothed simple moving average that gives more weight to the middle of the lookback period.",
201    usage: "Use for extremely smooth trend identification. TRIMA is significantly smoother than a standard SMA but introduces more lag; it is ideal for identifying long-term cycles.",
202    keywords: &["moving-average", "smoothing", "classic"],
203    ehlers_summary: "The Triangular Moving Average is an SMA of an SMA. For a period N, it averages the values over N/2 periods twice. This results in a weight distribution that is triangular, peaking at the center of the window, making it very effective at filtering out high-frequency noise. — StockCharts ChartSchool",
204    params: &[ParamDef { name: "timeperiod", default: "30", description: "Smoothing period" }],
205    formula_source: "https://www.tradingview.com/support/solutions/43000591273-triangular-moving-average-tma/",
206    formula_latex: r#"
207\[
208TRIMA = SMA(SMA(Price, n/2), n/2)
209\]
210"#,
211    gold_standard_file: "trima.json",
212    category: "Classic",
213};
214
215pub const T3_METADATA: IndicatorMetadata = IndicatorMetadata {
216    name: "Tilson T3 Moving Average",
217    description: "A smooth, low-lag moving average that uses multiple exponential smoothing.",
218    usage: "Use for trend following in noisy markets. T3 offers a superior balance between lag reduction and smoothness compared to DEMA or TEMA.",
219    keywords: &["moving-average", "smoothing", "lag-reduction", "classic"],
220    ehlers_summary: "Developed by Tim Tilson in 1998, the T3 moving average uses a 'v-factor' (volume factor) to control how much the indicator reacts to price changes. It is essentially a sextuple EMA smoothing process that provides a very smooth curve with remarkably little lag. — Technical Analysis of Stocks & Commodities",
221    params: &[
222        ParamDef { name: "timeperiod", default: "5", description: "Smoothing period" },
223        ParamDef { name: "v_factor", default: "0.7", description: "Volume factor (0.0 to 1.0)" },
224    ],
225    formula_source: "https://www.tradingview.com/script/667W2a8n-T3-Moving-Average/",
226    formula_latex: r#"
227\[
228e1 = EMA(Price, n) \\ e2 = EMA(e1, n) \\ \dots \\ e6 = EMA(e5, n) \\ T3 = c1 \times e6 + c2 \times e5 + c3 \times e4 + c4 \times e3
229\]
230"#,
231    gold_standard_file: "t3.json",
232    category: "Classic",
233};
234
235pub const BBANDS_METADATA: IndicatorMetadata = IndicatorMetadata {
236    name: "Bollinger Bands",
237    description: "A volatility indicator consisting of a middle SMA and two outer bands based on standard deviation.",
238    usage: "Use to identify overbought/oversold levels and volatility breakouts. Prices near the upper band suggest overbought conditions, while prices near the lower band suggest oversold conditions. Narrowing bands (The Squeeze) often precede large price moves.",
239    keywords: &["volatility", "trend", "classic", "bands"],
240    ehlers_summary: "Developed by John Bollinger in the 1980s, Bollinger Bands adapt to volatility by using standard deviation. The middle band is typically a 20-period SMA, and the outer bands are set 2 standard deviations away. This ensures that 95% of price action typically stays within the bands, making escapes highly significant. — BollingerOnBollingerBands.com",
241    params: &[
242        ParamDef {
243            name: "timeperiod",
244            default: "20",
245            description: "SMA period",
246        },
247        ParamDef {
248            name: "nbdevup",
249            default: "2.0",
250            description: "Upper deviation multiplier",
251        },
252        ParamDef {
253            name: "nbdevdn",
254            default: "2.0",
255            description: "Lower deviation multiplier",
256        },
257    ],
258    formula_source: "https://www.investopedia.com/terms/b/bollingerbands.asp",
259    formula_latex: r#"
260\[
261Middle = SMA(n) \\ Upper = Middle + (k \times \sigma) \\ Lower = Middle - (k \times \sigma)
262\]
263"#,
264    gold_standard_file: "bbands.json",
265    category: "Classic",
266};
267
268pub const SAR_METADATA: IndicatorMetadata = IndicatorMetadata {
269    name: "Parabolic SAR",
270    description: "A trend-following indicator used to determine price direction and potential reversals.",
271    usage: "Use for setting trailing stop losses and identifying trend reversals. Dots below price indicate an uptrend, while dots above price indicate a downtrend.",
272    keywords: &["trend", "classic", "stop-loss", "wilder"],
273    ehlers_summary: "Developed by J. Welles Wilder, the Parabolic Stop and Reverse (SAR) uses an acceleration factor that increases as the trend persists. This 'parabolic' nature allows the indicator to stay close to price action and provide timely exit signals when a trend exhausts. — StockCharts ChartSchool",
274    params: &[
275        ParamDef {
276            name: "acceleration",
277            default: "0.02",
278            description: "Acceleration factor",
279        },
280        ParamDef {
281            name: "maximum",
282            default: "0.2",
283            description: "Maximum acceleration",
284        },
285    ],
286    formula_source: "https://www.investopedia.com/terms/p/parabolicindicator.asp",
287    formula_latex: r#"
288\[
289SAR_{t+1} = SAR_t + AF \times (EP - SAR_t)
290\]
291"#,
292    gold_standard_file: "sar.json",
293    category: "Classic",
294};
295
296pub const MAMA_METADATA: IndicatorMetadata = IndicatorMetadata {
297    name: "MESA Adaptive Moving Average (MAMA)",
298    description: "A moving average that adapts to price movement based on the rate of change of phase.",
299    usage: "Use as a highly responsive moving average that virtually eliminates overshoot while providing rapid response to price changes. The companion 'FAMA' (Following Adaptive Moving Average) provides a secondary line for crossover signals.",
300    keywords: &["moving-average", "adaptive", "ehlers", "dsp", "phase"],
301    ehlers_summary: "MAMA adapts to the price movement based on the Hilbert Transform phase rate of change. It provides a unique combination of fast response to price changes while remaining smooth during congested market periods. It is one of the most sophisticated adaptive moving averages available. — Rocket Science for Traders",
302    params: &[
303        ParamDef {
304            name: "fastlimit",
305            default: "0.5",
306            description: "Fast limit",
307        },
308        ParamDef {
309            name: "slowlimit",
310            default: "0.05",
311            description: "Slow limit",
312        },
313    ],
314    formula_source: "http://www.mesasoftware.com/Papers/MAMA.pdf",
315    formula_latex: r#"
316\[
317\alpha = \frac{\text{FastLimit}}{\text{PhaseRate}}
318\]
319"#,
320    gold_standard_file: "mama.json",
321    category: "Ehlers DSP",
322};