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