quantwave_core/indicators/
volatility.rs1use 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
21pub struct TrueRange {
22 prev_close: Option<f64>,
23}
24
25impl TrueRange {
26 pub fn new() -> Self {
27 Self::default()
28 }
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 if high.is_nan() || low.is_nan() || close.is_nan() {
36 return f64::NAN;
37 }
38 let tr = match self.prev_close {
39 Some(pc) => {
40 let h_l = high - low;
41 let h_pc = (high - pc).abs();
42 let l_pc = (low - pc).abs();
43 h_l.max(h_pc).max(l_pc)
44 }
45 None => high - low,
46 };
47 self.prev_close = Some(close);
48 tr
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct ATR {
72 tr: TrueRange,
73 smoothing: EMA,
74}
75
76impl ATR {
77 pub fn new(period: usize) -> Self {
78 Self {
79 tr: TrueRange::default(),
80 smoothing: EMA::new(period),
81 }
82 }
83}
84
85impl Next<(f64, f64, f64)> for ATR {
86 type Output = f64;
87
88 fn next(&mut self, input: (f64, f64, f64)) -> Self::Output {
89 let tr = self.tr.next(input);
90 self.smoothing.next(tr)
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::traits::Next;
98 use proptest::prelude::*;
99
100 proptest! {
101 #[test]
102 fn test_ta_atr_parity(
103 h in prop::collection::vec(1.0..100.0, 1..100),
104 l in prop::collection::vec(1.0..100.0, 1..100),
105 c in prop::collection::vec(1.0..100.0, 1..100)
106 ) {
107 let len = h.len().min(l.len()).min(c.len());
108 if len == 0 { return Ok(()); }
109 let mut high = Vec::with_capacity(len);
110 let mut low = Vec::with_capacity(len);
111 let mut close = Vec::with_capacity(len);
112 for i in 0..len {
113 let v_h: f64 = h[i];
114 let v_l: f64 = l[i];
115 let v_c: f64 = c[i];
116 high.push(v_h.max(v_l).max(v_c));
117 low.push(v_h.min(v_l).min(v_c));
118 close.push(v_c);
119 }
120
121 let period = 14;
122 let mut ta_atr = TaATR::new(period);
123 let streaming_results: Vec<f64> = (0..len).map(|i| ta_atr.next((high[i], low[i], close[i]))).collect();
124 let batch_results = talib_rs::volatility::atr(&high, &low, &close, period).unwrap_or_else(|_| vec![f64::NAN; len]);
125
126 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
127 if s.is_nan() {
128 assert!(b.is_nan());
129 } else {
130 approx::assert_relative_eq!(s, b, epsilon = 1e-6);
131 }
132 }
133 }
134
135 #[test]
136 fn test_ta_trange_parity(
137 h in prop::collection::vec(1.0..100.0, 1..100),
138 l in prop::collection::vec(1.0..100.0, 1..100),
139 c in prop::collection::vec(1.0..100.0, 1..100)
140 ) {
141 let len = h.len().min(l.len()).min(c.len());
142 if len == 0 { return Ok(()); }
143 let mut high = Vec::with_capacity(len);
144 let mut low = Vec::with_capacity(len);
145 let mut close = Vec::with_capacity(len);
146 for i in 0..len {
147 let v_h: f64 = h[i];
148 let v_l: f64 = l[i];
149 let v_c: f64 = c[i];
150 high.push(v_h.max(v_l).max(v_c));
151 low.push(v_h.min(v_l).min(v_c));
152 close.push(v_c);
153 }
154
155 let mut ta_tr = TaTRANGE::new();
156 let streaming_results: Vec<f64> = (0..len).map(|i| ta_tr.next((high[i], low[i], close[i]))).collect();
157 let batch_results = talib_rs::volatility::trange(&high, &low, &close).unwrap_or_else(|_| vec![f64::NAN; len]);
158
159 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
160 if s.is_nan() {
161 assert!(b.is_nan());
162 } else {
163 approx::assert_relative_eq!(s, b, epsilon = 1e-6);
164 }
165 }
166 }
167 }
168}
169
170pub const TRUE_RANGE_METADATA: IndicatorMetadata = IndicatorMetadata {
171 name: "True Range",
172 description: "True Range measures daily volatility.",
173 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.",
174 keywords: &["volatility", "atr", "classic", "range"],
175 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.",
176 params: &[],
177 formula_source: "https://www.investopedia.com/terms/a/atr.asp",
178 formula_latex: r#"
179\[
180TR = \max(H - L, |H - C_{t-1}|, |L - C_{t-1}|)
181\]
182"#,
183 gold_standard_file: "true_range.json",
184 category: "Classic",
185};
186
187pub const ATR_METADATA: IndicatorMetadata = IndicatorMetadata {
188 name: "Average True Range",
189 description: "ATR represents the average of true ranges over a specified period. CONVENTION: the `Atr` streaming class and the `atr(period, high, low, close)` batch function smooth true range with an EMA (alpha = 2/(period+1)), NOT Wilder's RMA. For TA-Lib/TradingView-identical ATR use `ta_atr`. See the Average True Range indicator guide for the full surface-by-surface breakdown.",
190 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. Check `quantwave.conventions(\"atr\")` before reconciling values against TA-Lib, TradingView, or pandas.",
191 keywords: &[
192 "volatility",
193 "atr",
194 "classic",
195 "range",
196 "convention-divergence",
197 ],
198 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.",
199 params: &[ParamDef {
200 name: "period",
201 default: "14",
202 description: "Smoothing period",
203 }],
204 formula_source: "https://www.investopedia.com/terms/a/atr.asp (documents Wilder's RMA — implemented by `ta_atr`; no source recorded for the EMA-smoothed `Atr`)",
209 formula_latex: r#"
210\[
211TR_t = \max(H_t - L_t, |H_t - C_{t-1}|, |L_t - C_{t-1}|)
212\]
213\[
214\text{Atr / atr() (EMA variant): } ATR_t = \alpha TR_t + (1-\alpha) ATR_{t-1}, \quad \alpha = \frac{2}{n+1}
215\]
216\[
217\text{ta\_atr (Wilder RMA, TA-Lib): } ATR_t = \frac{ATR_{t-1} \times (n-1) + TR_t}{n}
218\]
219"#,
220 gold_standard_file: "atr.json",
221 category: "Classic",
222};
223
224pub const NATR_METADATA: IndicatorMetadata = IndicatorMetadata {
225 name: "Normalized Average True Range (NATR)",
226 description: "A normalized version of ATR that represents volatility as a percentage of price.",
227 usage: "Use to compare volatility across different securities with varying price levels. NATR allows for normalized risk assessment and position sizing.",
228 keywords: &["volatility", "atr", "normalization", "classic"],
229 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",
230 params: &[ParamDef {
231 name: "timeperiod",
232 default: "14",
233 description: "Smoothing period",
234 }],
235 formula_source: "https://www.tradingtechnologies.com/help/x-study/technical-indicator-definitions/normalized-average-true-range-natr/",
236 formula_latex: r#"
237\[
238NATR = \frac{ATR(n)}{Close} \times 100
239\]
240"#,
241 gold_standard_file: "natr.json",
242 category: "Classic",
243};