Skip to main content

quantwave_core/indicators/
overlap.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2pub use crate::indicators::incremental::bbands::BBANDS;
3pub use crate::indicators::incremental::dema::DEMA;
4impl From<usize> for DEMA {
5    fn from(p: usize) -> Self {
6        Self::new(p)
7    }
8}
9pub use crate::indicators::incremental::overlap_ta::{KAMA, MIDPOINT, MIDPRICE, T3, TRIMA};
10impl From<usize> for TRIMA {
11    fn from(p: usize) -> Self {
12        Self::new(p)
13    }
14}
15impl From<usize> for KAMA {
16    fn from(p: usize) -> Self {
17        Self::new(p)
18    }
19}
20impl From<usize> for MIDPOINT {
21    fn from(p: usize) -> Self {
22        Self::new(p)
23    }
24}
25impl From<usize> for MIDPRICE {
26    fn from(p: usize) -> Self {
27        Self::new(p)
28    }
29}
30pub use crate::indicators::mama::MAMA;
31pub use crate::indicators::incremental::sar::{SAR, SAREXT};
32pub use crate::indicators::incremental::mavp::MAVP;
33pub use crate::indicators::incremental::hilbert_ta::HT_TRENDLINE;
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38    use crate::traits::Next;
39    use proptest::prelude::*;
40
41    proptest! {
42        #[test]
43        fn test_dema_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
44            let period = 10;
45            let mut dema = DEMA::new(period);
46            let streaming_results: Vec<f64> = input.iter().map(|&x| dema.next(x)).collect();
47            let batch_results = talib_rs::overlap::dema(&input, period).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
48
49            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
50                if s.is_nan() {
51                    assert!(b.is_nan());
52                } else {
53                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
54                }
55            }
56        }
57
58        #[test]
59        fn test_trima_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
60            let period = 10;
61            let mut trima = TRIMA::new(period);
62            let streaming_results: Vec<f64> = input.iter().map(|&x| trima.next(x)).collect();
63            let batch_results = talib_rs::overlap::trima(&input, period).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
64
65            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
66                if s.is_nan() {
67                    assert!(b.is_nan());
68                } else {
69                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
70                }
71            }
72        }
73
74        #[test]
75        fn test_kama_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
76            let period = 10;
77            let mut kama = KAMA::new(period);
78            let streaming_results: Vec<f64> = input.iter().map(|&x| kama.next(x)).collect();
79            let batch_results = talib_rs::overlap::kama(&input, period).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
80
81            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
82                if s.is_nan() {
83                    assert!(b.is_nan());
84                } else {
85                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
86                }
87            }
88        }
89
90        #[test]
91        fn test_t3_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
92            let period = 10;
93            let v_factor = 0.7;
94            let mut t3 = T3::new(period, v_factor);
95            let streaming_results: Vec<f64> = input.iter().map(|&x| t3.next(x)).collect();
96            let batch_results = talib_rs::overlap::t3(&input, period, v_factor).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
97
98            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
99                if s.is_nan() {
100                    assert!(b.is_nan());
101                } else {
102                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
103                }
104            }
105        }
106
107        #[test]
108        fn test_bbands_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
109            let period = 10;
110            let nbdevup = 2.0;
111            let nbdevdn = 2.0;
112            let matype = talib_rs::MaType::Sma;
113            let mut bbands = BBANDS::new(period, nbdevup, nbdevdn, matype);
114            let streaming_results: Vec<(f64, f64, f64)> = input.iter().map(|&x| bbands.next(x)).collect();
115            let (b_upper, b_middle, b_lower) = talib_rs::overlap::bbands(&input, period, nbdevup, nbdevdn, matype).unwrap_or_else(|_| {
116                (vec![f64::NAN; input.len()], vec![f64::NAN; input.len()], vec![f64::NAN; input.len()])
117            });
118
119            for (i, (s_upper, s_middle, s_lower)) in streaming_results.into_iter().enumerate() {
120                if s_upper.is_nan() {
121                    assert!(b_upper[i].is_nan());
122                } else {
123                    approx::assert_relative_eq!(s_upper, b_upper[i], epsilon = 1e-6);
124                }
125                if s_middle.is_nan() {
126                    assert!(b_middle[i].is_nan());
127                } else {
128                    approx::assert_relative_eq!(s_middle, b_middle[i], epsilon = 1e-6);
129                }
130                if s_lower.is_nan() {
131                    assert!(b_lower[i].is_nan());
132                } else {
133                    approx::assert_relative_eq!(s_lower, b_lower[i], epsilon = 1e-6);
134                }
135            }
136        }
137
138        #[test]
139        fn test_sar_parity(
140            h in prop::collection::vec(10.0..100.0, 1..100),
141            l in prop::collection::vec(10.0..100.0, 1..100)
142        ) {
143            let len = h.len().min(l.len());
144            if len == 0 { return Ok(()); }
145            let mut high = Vec::with_capacity(len);
146            let mut low = Vec::with_capacity(len);
147            for i in 0..len {
148                let v_h: f64 = h[i];
149                let v_l: f64 = l[i];
150                high.push(v_h.max(v_l));
151                low.push(v_h.min(v_l));
152            }
153
154            let accel = 0.02;
155            let max = 0.2;
156            let mut sar = SAR::new(accel, max);
157            let streaming_results: Vec<f64> = (0..len).map(|i| sar.next((high[i], low[i]))).collect();
158            let batch_results = talib_rs::overlap::sar(&high, &low, accel, max).unwrap_or_else(|_| vec![f64::NAN; len]);
159
160            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
161                if s.is_nan() {
162                    assert!(b.is_nan());
163                } else {
164                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
165                }
166            }
167        }
168    }
169}
170
171pub const DEMA_METADATA: IndicatorMetadata = IndicatorMetadata {
172    name: "Double Exponential Moving Average (DEMA)",
173    description: "A fast-acting moving average that reduces lag by using two exponential moving averages.",
174    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.",
175    keywords: &["moving-average", "smoothing", "lag-reduction", "classic"],
176    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",
177    params: &[ParamDef {
178        name: "timeperiod",
179        default: "30",
180        description: "Smoothing period",
181    }],
182    formula_source: "https://www.investopedia.com/terms/d/double-exponential-moving-average.asp",
183    formula_latex: r#"
184\[
185DEMA = 2 \times EMA - EMA(EMA)
186\]
187"#,
188    gold_standard_file: "dema.json",
189    category: "Classic",
190};
191
192pub const TRIMA_METADATA: IndicatorMetadata = IndicatorMetadata {
193    name: "Triangular Moving Average (TRIMA)",
194    description: "A double-smoothed simple moving average that gives more weight to the middle of the lookback period.",
195    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.",
196    keywords: &["moving-average", "smoothing", "classic"],
197    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",
198    params: &[ParamDef {
199        name: "timeperiod",
200        default: "30",
201        description: "Smoothing period",
202    }],
203    formula_source: "https://www.tradingview.com/support/solutions/43000591273-triangular-moving-average-tma/",
204    formula_latex: r#"
205\[
206TRIMA = SMA(SMA(Price, n/2), n/2)
207\]
208"#,
209    gold_standard_file: "trima.json",
210    category: "Classic",
211};
212
213pub const T3_METADATA: IndicatorMetadata = IndicatorMetadata {
214    name: "Tilson T3 Moving Average",
215    description: "A smooth, low-lag moving average that uses multiple exponential smoothing.",
216    usage: "Use for trend following in noisy markets. T3 offers a superior balance between lag reduction and smoothness compared to DEMA or TEMA.",
217    keywords: &["moving-average", "smoothing", "lag-reduction", "classic"],
218    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",
219    params: &[
220        ParamDef {
221            name: "timeperiod",
222            default: "5",
223            description: "Smoothing period",
224        },
225        ParamDef {
226            name: "v_factor",
227            default: "0.7",
228            description: "Volume factor (0.0 to 1.0)",
229        },
230    ],
231    formula_source: "https://www.tradingview.com/script/667W2a8n-T3-Moving-Average/",
232    formula_latex: r#"
233\[
234e1 = 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
235\]
236"#,
237    gold_standard_file: "t3.json",
238    category: "Classic",
239};
240
241pub const BBANDS_METADATA: IndicatorMetadata = IndicatorMetadata {
242    name: "Bollinger Bands",
243    description: "A volatility indicator consisting of a middle SMA and two outer bands based on standard deviation.",
244    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.",
245    keywords: &["volatility", "trend", "classic", "bands"],
246    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",
247    params: &[
248        ParamDef {
249            name: "timeperiod",
250            default: "20",
251            description: "SMA period",
252        },
253        ParamDef {
254            name: "nbdevup",
255            default: "2.0",
256            description: "Upper deviation multiplier",
257        },
258        ParamDef {
259            name: "nbdevdn",
260            default: "2.0",
261            description: "Lower deviation multiplier",
262        },
263    ],
264    formula_source: "https://www.investopedia.com/terms/b/bollingerbands.asp",
265    formula_latex: r#"
266\[
267Middle = SMA(n) \\ Upper = Middle + (k \times \sigma) \\ Lower = Middle - (k \times \sigma)
268\]
269"#,
270    gold_standard_file: "bbands.json",
271    category: "Classic",
272};
273
274pub const SAR_METADATA: IndicatorMetadata = IndicatorMetadata {
275    name: "Parabolic SAR",
276    description: "A trend-following indicator used to determine price direction and potential reversals.",
277    usage: "Use for setting trailing stop losses and identifying trend reversals. Dots below price indicate an uptrend, while dots above price indicate a downtrend.",
278    keywords: &["trend", "classic", "stop-loss", "wilder"],
279    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",
280    params: &[
281        ParamDef {
282            name: "acceleration",
283            default: "0.02",
284            description: "Acceleration factor",
285        },
286        ParamDef {
287            name: "maximum",
288            default: "0.2",
289            description: "Maximum acceleration",
290        },
291    ],
292    formula_source: "https://www.investopedia.com/terms/p/parabolicindicator.asp",
293    formula_latex: r#"
294\[
295SAR_{t+1} = SAR_t + AF \times (EP - SAR_t)
296\]
297"#,
298    gold_standard_file: "sar.json",
299    category: "Classic",
300};
301
302pub use crate::indicators::mama::MAMA_METADATA;