Skip to main content

quantwave_core/indicators/
volatility.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::EMA;
3use crate::traits::Next;
4
5talib_3_in_1_out!(TaATR, talib_rs::volatility::atr, timeperiod: usize);
6impl From<usize> for TaATR {
7    fn from(p: usize) -> Self {
8        Self::new(p)
9    }
10}
11talib_3_in_1_out!(TaNATR, talib_rs::volatility::natr, timeperiod: usize);
12impl From<usize> for TaNATR {
13    fn from(p: usize) -> Self {
14        Self::new(p)
15    }
16}
17talib_3_in_1_out!(TaTRANGE, talib_rs::volatility::trange);
18impl Default for TaTRANGE {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24/// True Range (TR)
25#[derive(Debug, Clone, Default)]
26pub struct TrueRange {
27    prev_close: Option<f64>,
28}
29
30impl Next<(f64, f64, f64)> for TrueRange {
31    type Output = f64;
32
33    fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
34        let tr = match self.prev_close {
35            Some(pc) => {
36                let h_l = high - low;
37                let h_pc = (high - pc).abs();
38                let l_pc = (low - pc).abs();
39                h_l.max(h_pc).max(l_pc)
40            }
41            None => high - low,
42        };
43        self.prev_close = Some(close);
44        tr
45    }
46}
47
48/// Average True Range (ATR)
49#[derive(Debug, Clone)]
50pub struct ATR {
51    tr: TrueRange,
52    smoothing: EMA,
53}
54
55impl ATR {
56    pub fn new(period: usize) -> Self {
57        Self {
58            tr: TrueRange::default(),
59            smoothing: EMA::new(period),
60        }
61    }
62}
63
64impl Next<(f64, f64, f64)> for ATR {
65    type Output = f64;
66
67    fn next(&mut self, input: (f64, f64, f64)) -> Self::Output {
68        let tr = self.tr.next(input);
69        self.smoothing.next(tr)
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::traits::Next;
77    use proptest::prelude::*;
78
79    proptest! {
80        #[test]
81        fn test_ta_atr_parity(
82            h in prop::collection::vec(1.0..100.0, 1..100),
83            l in prop::collection::vec(1.0..100.0, 1..100),
84            c in prop::collection::vec(1.0..100.0, 1..100)
85        ) {
86            let len = h.len().min(l.len()).min(c.len());
87            if len == 0 { return Ok(()); }
88            let mut high = Vec::with_capacity(len);
89            let mut low = Vec::with_capacity(len);
90            let mut close = Vec::with_capacity(len);
91            for i in 0..len {
92                let v_h: f64 = h[i];
93                let v_l: f64 = l[i];
94                let v_c: f64 = c[i];
95                high.push(v_h.max(v_l).max(v_c));
96                low.push(v_h.min(v_l).min(v_c));
97                close.push(v_c);
98            }
99
100            let period = 14;
101            let mut ta_atr = TaATR::new(period);
102            let streaming_results: Vec<f64> = (0..len).map(|i| ta_atr.next((high[i], low[i], close[i]))).collect();
103            let batch_results = talib_rs::volatility::atr(&high, &low, &close, period).unwrap_or_else(|_| vec![f64::NAN; len]);
104
105            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
106                if s.is_nan() {
107                    assert!(b.is_nan());
108                } else {
109                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
110                }
111            }
112        }
113
114        #[test]
115        fn test_ta_trange_parity(
116            h in prop::collection::vec(1.0..100.0, 1..100),
117            l in prop::collection::vec(1.0..100.0, 1..100),
118            c in prop::collection::vec(1.0..100.0, 1..100)
119        ) {
120            let len = h.len().min(l.len()).min(c.len());
121            if len == 0 { return Ok(()); }
122            let mut high = Vec::with_capacity(len);
123            let mut low = Vec::with_capacity(len);
124            let mut close = Vec::with_capacity(len);
125            for i in 0..len {
126                let v_h: f64 = h[i];
127                let v_l: f64 = l[i];
128                let v_c: f64 = c[i];
129                high.push(v_h.max(v_l).max(v_c));
130                low.push(v_h.min(v_l).min(v_c));
131                close.push(v_c);
132            }
133
134            let mut ta_tr = TaTRANGE::new();
135            let streaming_results: Vec<f64> = (0..len).map(|i| ta_tr.next((high[i], low[i], close[i]))).collect();
136            let batch_results = talib_rs::volatility::trange(&high, &low, &close).unwrap_or_else(|_| vec![f64::NAN; len]);
137
138            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
139                if s.is_nan() {
140                    assert!(b.is_nan());
141                } else {
142                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
143                }
144            }
145        }
146    }
147}
148
149pub const TRUE_RANGE_METADATA: IndicatorMetadata = IndicatorMetadata {
150    name: "True Range",
151    description: "True Range measures daily volatility.",
152    params: &[],
153    formula_source: "https://www.investopedia.com/terms/a/atr.asp",
154    formula_latex: r#"
155\[
156TR = \max(H - L, |H - C_{t-1}|, |L - C_{t-1}|)
157\]
158"#,
159    gold_standard_file: "true_range.json",
160    category: "Classic",
161};
162
163pub const ATR_METADATA: IndicatorMetadata = IndicatorMetadata {
164    name: "Average True Range",
165    description: "ATR represents the average of true ranges over a specified period.",
166    params: &[ParamDef {
167        name: "period",
168        default: "14",
169        description: "Smoothing period",
170    }],
171    formula_source: "https://www.investopedia.com/terms/a/atr.asp",
172    formula_latex: r#"
173\[
174ATR = \frac{ATR_{t-1} \times (n-1) + TR_t}{n}
175\]
176"#,
177    gold_standard_file: "atr.json",
178    category: "Classic",
179};