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