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    usage: "Use as the foundational volatility module providing ATR, True Range, and related volatility measures used by higher-level indicators such as SuperTrend and Keltner Channels.",
153    keywords: &["volatility", "atr", "classic", "range"],
154    ehlers_summary: "Average True Range, developed by J. Welles Wilder in New Concepts in Technical Trading Systems (1978), measures the average of the true range over N bars. True Range accounts for overnight gaps by taking the maximum of: current high minus low, current high minus prior close, prior close minus current low. It remains the industry standard raw volatility measure.",
155    params: &[],
156    formula_source: "https://www.investopedia.com/terms/a/atr.asp",
157    formula_latex: r#"
158\[
159TR = \max(H - L, |H - C_{t-1}|, |L - C_{t-1}|)
160\]
161"#,
162    gold_standard_file: "true_range.json",
163    category: "Classic",
164};
165
166pub const ATR_METADATA: IndicatorMetadata = IndicatorMetadata {
167    name: "Average True Range",
168    description: "ATR represents the average of true ranges over a specified period.",
169    usage: "Use as the foundational volatility module providing ATR, True Range, and related volatility measures used by higher-level indicators such as SuperTrend and Keltner Channels.",
170    keywords: &["volatility", "atr", "classic", "range"],
171    ehlers_summary: "Average True Range, developed by J. Welles Wilder in New Concepts in Technical Trading Systems (1978), measures the average of the true range over N bars. True Range accounts for overnight gaps by taking the maximum of: current high minus low, current high minus prior close, prior close minus current low. It remains the industry standard raw volatility measure.",
172    params: &[ParamDef {
173        name: "period",
174        default: "14",
175        description: "Smoothing period",
176    }],
177    formula_source: "https://www.investopedia.com/terms/a/atr.asp",
178    formula_latex: r#"
179\[
180ATR = \frac{ATR_{t-1} \times (n-1) + TR_t}{n}
181\]
182"#,
183    gold_standard_file: "atr.json",
184    category: "Classic",
185};
186
187pub const NATR_METADATA: IndicatorMetadata = IndicatorMetadata {
188    name: "Normalized Average True Range (NATR)",
189    description: "A normalized version of ATR that represents volatility as a percentage of price.",
190    usage: "Use to compare volatility across different securities with varying price levels. NATR allows for normalized risk assessment and position sizing.",
191    keywords: &["volatility", "atr", "normalization", "classic"],
192    ehlers_summary: "Normalized ATR (NATR) was developed to allow traders to compare the volatility of high-priced stocks with low-priced stocks. By dividing the ATR by the closing price and multiplying by 100, the result is a percentage that can be used consistently across all assets. — TA-Lib Documentation",
193    params: &[ParamDef { name: "timeperiod", default: "14", description: "Smoothing period" }],
194    formula_source: "https://www.tradingtechnologies.com/help/x-study/technical-indicator-definitions/normalized-average-true-range-natr/",
195    formula_latex: r#"
196\[
197NATR = \frac{ATR(n)}{Close} \times 100
198\]
199"#,
200    gold_standard_file: "natr.json",
201    category: "Classic",
202};