quantwave_core/indicators/
volatility.rs1use 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#[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#[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};