Skip to main content

quantwave_core/indicators/incremental/
trange.rs

1//! Native TaTRANGE / TaNATR — TA-Lib parity.
2
3use crate::indicators::incremental::ta_atr::TaATR;
4use crate::traits::Next;
5
6/// True Range (TRANGE) — first bar NaN, then standard TR.
7#[derive(Debug, Clone, Default)]
8#[allow(non_camel_case_types)]
9pub struct TaTRANGE {
10    prev_close: Option<f64>,
11    bars_seen: usize,
12}
13
14impl TaTRANGE {
15    pub fn new() -> Self {
16        Self::default()
17    }
18}
19
20impl Next<(f64, f64, f64)> for TaTRANGE {
21    type Output = f64;
22
23    fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
24        if self.bars_seen == 0 {
25            self.prev_close = Some(close);
26            self.bars_seen = 1;
27            return f64::NAN;
28        }
29        let pc = self.prev_close.unwrap();
30        let hl = high - low;
31        let hc = (high - pc).abs();
32        let lc = (low - pc).abs();
33        self.prev_close = Some(close);
34        self.bars_seen += 1;
35        hl.max(hc).max(lc)
36    }
37}
38
39/// Normalized ATR (NATR) — 100 * ATR / Close.
40#[derive(Debug, Clone)]
41#[allow(non_camel_case_types)]
42pub struct TaNATR {
43    pub timeperiod: usize,
44    atr: TaATR,
45}
46
47impl TaNATR {
48    pub fn new(timeperiod: usize) -> Self {
49        Self {
50            timeperiod,
51            atr: TaATR::new(timeperiod),
52        }
53    }
54}
55
56impl Next<(f64, f64, f64)> for TaNATR {
57    type Output = f64;
58
59    fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
60        let atr = self.atr.next((high, low, close));
61        if atr.is_nan() {
62            return f64::NAN;
63        }
64        if close == 0.0 {
65            0.0
66        } else {
67            (atr / close) * 100.0
68        }
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use proptest::prelude::*;
76
77    proptest! {
78        #[test]
79        fn test_ta_trange_parity(
80            highs in prop::collection::vec(1.0..100.0, 1..100),
81            lows in prop::collection::vec(1.0..100.0, 1..100),
82            closes in prop::collection::vec(1.0..100.0, 1..100)
83        ) {
84            let len = highs.len().min(lows.len()).min(closes.len());
85            if len < 3 { return Ok(()); }
86            let mut high = Vec::with_capacity(len);
87            let mut low = Vec::with_capacity(len);
88            let mut close = Vec::with_capacity(len);
89            for i in 0..len {
90                let hi: f64 = highs[i];
91                let lo: f64 = lows[i];
92                let cl: f64 = closes[i];
93                high.push(hi.max(lo).max(cl));
94                low.push(hi.min(lo).min(cl));
95                close.push(cl);
96            }
97            let mut tr = TaTRANGE::new();
98            let streaming: Vec<f64> = (0..len).map(|i| tr.next((high[i], low[i], close[i]))).collect();
99            let batch = talib_rs::volatility::trange(&high, &low, &close)
100                .unwrap_or_else(|_| vec![f64::NAN; len]);
101            for (s, b) in streaming.iter().zip(batch.iter()) {
102                if s.is_nan() { assert!(b.is_nan()); }
103                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
104            }
105        }
106
107        #[test]
108        fn test_ta_natr_parity(
109            highs in prop::collection::vec(1.0..100.0, 1..100),
110            lows in prop::collection::vec(1.0..100.0, 1..100),
111            closes in prop::collection::vec(1.0..100.0, 1..100)
112        ) {
113            let len = highs.len().min(lows.len()).min(closes.len());
114            if len < 20 { return Ok(()); }
115            let mut high = Vec::with_capacity(len);
116            let mut low = Vec::with_capacity(len);
117            let mut close = Vec::with_capacity(len);
118            for i in 0..len {
119                let hi: f64 = highs[i];
120                let lo: f64 = lows[i];
121                let cl: f64 = closes[i];
122                high.push(hi.max(lo).max(cl));
123                low.push(hi.min(lo).min(cl));
124                close.push(cl);
125            }
126            let period = 14;
127            let mut natr = TaNATR::new(period);
128            let streaming: Vec<f64> = (0..len).map(|i| natr.next((high[i], low[i], close[i]))).collect();
129            let batch = talib_rs::volatility::natr(&high, &low, &close, period)
130                .unwrap_or_else(|_| vec![f64::NAN; len]);
131            for (s, b) in streaming.iter().zip(batch.iter()) {
132                if s.is_nan() { assert!(b.is_nan()); }
133                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
134            }
135        }
136    }
137}