Skip to main content

quantwave_core/indicators/
volatility.rs

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