Skip to main content

finance_solution/stocks/ta/
atr.rs

1//! # Average True Range (ATR)
2//!
3//! Wilder ATR of high / low / close:
4//!
5//! ```text
6//! TR[i]  = max( high−low, |high−close_prev|, |low−close_prev| )
7//! ATR[i] = Wilder smooth of TR over `period`
8//!          (seed = SMA of first `period` true ranges)
9//! ```
10//!
11//! Default pack: **period 14** ([`AtrParams::period_14`]).  
12//! Keltner channels reuse this definition internally for their ATR leg.
13//!
14//! Also public: [`true_range_series`] (per-bar TR) and [`natr`] (normalized ATR = `100 * ATR / close`).
15//!
16//! | Helper | Use when |
17//! |--------|----------|
18//! | [`atr`] | Absolute volatility (price units) for stops / Keltner / Supertrend |
19//! | [`natr`] | Compare volatility **across names** (percent of price) |
20//! | [`true_range_series`] | Bar-level range including gaps (building block / research) |
21//!
22//! Pair **ATR** with Supertrend, Keltner, position sizing; pair **NATR** with cross-sectional
23//! screens; pair **TR** with custom range stats.
24//!
25//! ---
26//!
27//! ## Trading perspective
28//!
29//! | Use | Habit |
30//! |-----|-------|
31//! | Volatility level | Wide ATR → large stops / size down |
32//! | Breakout filters | Move in ATR units |
33//! | Keltner width | `mult * ATR` around EMA |
34//!
35//! ---
36//!
37//! ## Engineering perspective
38//!
39//! [`AtrParams`] → [`ValidatedAtr`] / [`atr`] → [`AtrState`] → [`atr_solution`].  
40//! First ATR at index `period − 1` when bar 0 has TR = high−low only (no prior close).
41//!
42//! ## Word problem
43//!
44//! > Constant 2-point range bars, no gaps. What is ATR(14) after warm-up?
45//!
46//! ≈ **2.0**.
47//!
48//! ```
49//! use finance_solution::stocks::ta::{atr, AtrParams};
50//! let n = 30usize;
51//! let high: Vec<_> = (0..n).map(|_| 102.0).collect();
52//! let low: Vec<_> = (0..n).map(|_| 100.0).collect();
53//! let close: Vec<_> = (0..n).map(|_| 101.0).collect();
54//! let s = atr(&high, &low, &close, AtrParams::period_14()).unwrap();
55//! assert!((s.last().unwrap() - 2.0).abs() < 1e-9);
56//! ```
57
58use crate::stocks::ta::common::{opt_cell, require_hlc, true_range};
59use crate::util::error::FinanceResult;
60use crate::util::primitives::PeriodLength;
61use crate::{columns_with_strings, print_table_locale_opt};
62
63/// ATR lookback pack (Wilder).
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
65pub struct AtrParams {
66    pub period: usize,
67}
68
69impl AtrParams {
70    pub const fn new(period: usize) -> Self {
71        Self { period }
72    }
73
74    pub const fn period_14() -> Self {
75        Self { period: 14 }
76    }
77
78    pub const fn period_10() -> Self {
79        Self { period: 10 }
80    }
81}
82
83/// Validated ATR config.
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
85pub struct ValidatedAtr {
86    params: AtrParams,
87}
88
89impl ValidatedAtr {
90    pub fn new(params: AtrParams) -> FinanceResult<Self> {
91        PeriodLength::new(params.period)?;
92        Ok(Self { params })
93    }
94
95    pub fn params(self) -> AtrParams {
96        self.params
97    }
98
99    pub fn compute(self, high: &[f64], low: &[f64], close: &[f64]) -> FinanceResult<AtrSeries> {
100        atr_validated(high, low, close, self)
101    }
102}
103
104#[derive(Clone, Debug, PartialEq)]
105pub struct AtrSeries {
106    pub atr: Vec<Option<f64>>,
107    pub params: AtrParams,
108}
109
110impl AtrSeries {
111    pub fn last(&self) -> Option<f64> {
112        self.atr.iter().rev().find_map(|x| *x)
113    }
114}
115
116#[derive(Clone, Debug)]
117pub struct AtrSolution {
118    series: AtrSeries,
119    close: Vec<f64>,
120    formula: String,
121    symbolic_formula: String,
122}
123
124impl AtrSolution {
125    pub fn series(&self) -> &AtrSeries {
126        &self.series
127    }
128    pub fn formula(&self) -> &str {
129        &self.formula
130    }
131    pub fn symbolic_formula(&self) -> &str {
132        &self.symbolic_formula
133    }
134
135    pub fn print_table(&self) {
136        self.print_table_locale_opt(None, None);
137    }
138
139    pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
140        self.print_table_locale_opt(Some(locale), Some(precision));
141    }
142
143    fn print_table_locale_opt(
144        &self,
145        locale: Option<&num_format::Locale>,
146        precision: Option<usize>,
147    ) {
148        let columns = columns_with_strings(&[
149            ("period", "i", true),
150            ("close", "f", true),
151            ("atr", "f", true),
152        ]);
153        let data = self
154            .close
155            .iter()
156            .enumerate()
157            .map(|(i, c)| vec![i.to_string(), c.to_string(), opt_cell(self.series.atr[i])])
158            .collect();
159        print_table_locale_opt(&columns, data, locale, precision);
160    }
161}
162
163/// Incremental Wilder ATR.
164#[derive(Clone, Debug, PartialEq)]
165pub struct AtrState {
166    params: AtrParams,
167    prev_close: Option<f64>,
168    atr: Option<f64>,
169    seed_tr: Vec<f64>,
170    last: Option<f64>,
171}
172
173impl AtrState {
174    pub fn new(params: AtrParams) -> FinanceResult<Self> {
175        PeriodLength::new(params.period)?;
176        Ok(Self {
177            params,
178            prev_close: None,
179            atr: None,
180            seed_tr: Vec::with_capacity(params.period),
181            last: None,
182        })
183    }
184
185    pub fn from_history(
186        params: AtrParams,
187        high: &[f64],
188        low: &[f64],
189        close: &[f64],
190    ) -> FinanceResult<Self> {
191        let mut s = Self::new(params)?;
192        let _ = s.push_bars(high, low, close)?;
193        Ok(s)
194    }
195
196    pub fn params(&self) -> AtrParams {
197        self.params
198    }
199
200    pub fn push(&mut self, high: f64, low: f64, close: f64) -> FinanceResult<Option<f64>> {
201        crate::util::error::require_finite("high", high)?;
202        crate::util::error::require_finite("low", low)?;
203        crate::util::error::require_finite("close", close)?;
204        if high < low {
205            return Err(crate::util::error::FinanceError::InvalidCashflow {
206                message: "high must be >= low",
207            });
208        }
209        let period = self.params.period;
210        let tr = true_range(high, low, self.prev_close);
211        let out = if self.atr.is_none() {
212            let _ = self.seed_tr.push(tr);
213            if self.seed_tr.len() == period {
214                let a = self.seed_tr.iter().sum::<f64>() / period as f64;
215                self.atr = Some(a);
216                self.last = Some(a);
217                self.last
218            } else {
219                None
220            }
221        } else {
222            let a = self.atr.unwrap();
223            let a = (a * (period as f64 - 1.0) + tr) / period as f64;
224            self.atr = Some(a);
225            self.last = Some(a);
226            self.last
227        };
228        self.prev_close = Some(close);
229        Ok(out)
230    }
231
232    pub fn push_bars(
233        &mut self,
234        high: &[f64],
235        low: &[f64],
236        close: &[f64],
237    ) -> FinanceResult<Vec<Option<f64>>> {
238        require_hlc(high, low, close)?;
239        let mut out = Vec::with_capacity(close.len());
240        for i in 0..close.len() {
241            out.push(self.push(high[i], low[i], close[i])?);
242        }
243        Ok(out)
244    }
245
246    pub fn last(&self) -> Option<f64> {
247        self.last
248    }
249
250    pub fn reset(&mut self) {
251        self.prev_close = None;
252        self.atr = None;
253        self.seed_tr.clear();
254        self.last = None;
255    }
256}
257
258pub fn atr(
259    high: &[f64],
260    low: &[f64],
261    close: &[f64],
262    params: AtrParams,
263) -> FinanceResult<AtrSeries> {
264    ValidatedAtr::new(params)?.compute(high, low, close)
265}
266
267/// Per-bar true range series (same length as inputs). Bar 0 uses `H−L` only.
268///
269/// ```
270/// use finance_solution::stocks::ta::true_range_series;
271/// let h = [10.0, 12.0];
272/// let l = [9.0, 10.0];
273/// let c = [9.5, 11.0];
274/// let tr = true_range_series(&h, &l, &c).unwrap();
275/// assert!((tr[0] - 1.0).abs() < 1e-12);
276/// assert!((tr[1] - 2.5).abs() < 1e-12); // max(2, |12-9.5|, |10-9.5|)
277/// ```
278pub fn true_range_series(high: &[f64], low: &[f64], close: &[f64]) -> FinanceResult<Vec<f64>> {
279    require_hlc(high, low, close)?;
280    let mut out = Vec::with_capacity(close.len());
281    let mut prev = None;
282    for i in 0..close.len() {
283        out.push(true_range(high[i], low[i], prev));
284        prev = Some(close[i]);
285    }
286    Ok(out)
287}
288
289/// Normalized ATR: `100 * ATR / close` when both defined and close ≠ 0.
290///
291/// ```
292/// use finance_solution::stocks::ta::{natr, AtrParams};
293/// let n = 30usize;
294/// let h: Vec<_> = (0..n).map(|_| 102.0).collect();
295/// let l: Vec<_> = (0..n).map(|_| 100.0).collect();
296/// let c: Vec<_> = (0..n).map(|_| 100.0).collect();
297/// let s = natr(&h, &l, &c, AtrParams::period_14()).unwrap();
298/// // ATR≈2 → NATR ≈ 100*2/100 = 2
299/// assert!((s.natr.last().unwrap().unwrap() - 2.0).abs() < 1e-9);
300/// ```
301pub fn natr(
302    high: &[f64],
303    low: &[f64],
304    close: &[f64],
305    params: AtrParams,
306) -> FinanceResult<NatrSeries> {
307    let a = atr(high, low, close, params)?;
308    let mut natr = Vec::with_capacity(close.len());
309    for i in 0..close.len() {
310        let v = match a.atr[i] {
311            Some(atr_v) if close[i] != 0.0 => Some(100.0 * atr_v / close[i]),
312            _ => None,
313        };
314        natr.push(v);
315    }
316    Ok(NatrSeries { natr, params })
317}
318
319/// Normalized ATR series.
320#[derive(Clone, Debug, PartialEq)]
321pub struct NatrSeries {
322    pub natr: Vec<Option<f64>>,
323    pub params: AtrParams,
324}
325
326impl NatrSeries {
327    pub fn last(&self) -> Option<f64> {
328        self.natr.iter().rev().find_map(|x| *x)
329    }
330}
331
332/// Incremental NATR (wraps [`AtrState`]).
333#[derive(Clone, Debug)]
334pub struct NatrState {
335    atr: AtrState,
336    last: Option<f64>,
337}
338
339impl NatrState {
340    pub fn new(params: AtrParams) -> FinanceResult<Self> {
341        Ok(Self {
342            atr: AtrState::new(params)?,
343            last: None,
344        })
345    }
346
347    pub fn from_history(
348        params: AtrParams,
349        high: &[f64],
350        low: &[f64],
351        close: &[f64],
352    ) -> FinanceResult<Self> {
353        let mut s = Self::new(params)?;
354        let _ = s.push_bars(high, low, close)?;
355        Ok(s)
356    }
357
358    pub fn params(&self) -> AtrParams {
359        self.atr.params
360    }
361
362    pub fn reset(&mut self) {
363        self.atr.reset();
364        self.last = None;
365    }
366
367    pub fn push(&mut self, high: f64, low: f64, close: f64) -> FinanceResult<Option<f64>> {
368        let a = self.atr.push(high, low, close)?;
369        let out = match a {
370            Some(atr_v) if close != 0.0 => Some(100.0 * atr_v / close),
371            _ => None,
372        };
373        self.last = out;
374        Ok(out)
375    }
376
377    pub fn push_bars(
378        &mut self,
379        high: &[f64],
380        low: &[f64],
381        close: &[f64],
382    ) -> FinanceResult<Vec<Option<f64>>> {
383        require_hlc(high, low, close)?;
384        let mut out = Vec::with_capacity(close.len());
385        for i in 0..close.len() {
386            out.push(self.push(high[i], low[i], close[i])?);
387        }
388        Ok(out)
389    }
390
391    pub fn last(&self) -> Option<f64> {
392        self.last
393    }
394}
395
396fn atr_validated(
397    high: &[f64],
398    low: &[f64],
399    close: &[f64],
400    eng: ValidatedAtr,
401) -> FinanceResult<AtrSeries> {
402    require_hlc(high, low, close)?;
403    let mut state = AtrState::new(eng.params)?;
404    let atr = state.push_bars(high, low, close)?;
405    Ok(AtrSeries {
406        atr,
407        params: eng.params,
408    })
409}
410
411/// # Examples
412/// ```
413/// use finance_solution::stocks::ta::{atr_solution, AtrParams};
414/// let high = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0];
415/// let low  = vec![ 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0];
416/// let close= vec![ 9.5, 10.5, 11.5, 12.5, 13.5, 14.5, 15.5, 16.5, 17.5, 18.5, 19.5, 20.5, 21.5, 22.5, 23.5];
417/// let sol = atr_solution(&high, &low, &close, AtrParams::period_14()).unwrap();
418/// assert!(sol.series().last().is_some());
419/// ```
420pub fn atr_solution(
421    high: &[f64],
422    low: &[f64],
423    close: &[f64],
424    params: AtrParams,
425) -> FinanceResult<AtrSolution> {
426    let series = atr(high, low, close, params)?;
427    Ok(AtrSolution {
428        series,
429        close: close.to_vec(),
430        formula: format!("ATR({}) = Wilder smooth of true range", params.period),
431        symbolic_formula: "ATR = Wilder(TR); TR = max(H-L, |H-Cprev|, |L-Cprev|)".to_string(),
432    })
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    #[test]
440    fn constant_range() {
441        let n = 30usize;
442        let high: Vec<_> = (0..n).map(|_| 102.0).collect();
443        let low: Vec<_> = (0..n).map(|_| 100.0).collect();
444        let close: Vec<_> = (0..n).map(|_| 101.0).collect();
445        let s = atr(&high, &low, &close, AtrParams::period_14()).unwrap();
446        assert!((s.last().unwrap() - 2.0).abs() < 1e-9);
447    }
448
449    #[test]
450    fn state_parity() {
451        let n = 40usize;
452        let high: Vec<_> = (0..n).map(|i| 101.0 + i as f64 * 0.1).collect();
453        let low: Vec<_> = (0..n).map(|i| 99.0 + i as f64 * 0.1).collect();
454        let close: Vec<_> = (0..n).map(|i| 100.0 + i as f64 * 0.1).collect();
455        let batch = atr(&high, &low, &close, AtrParams::period_10()).unwrap();
456        let st = AtrState::from_history(AtrParams::period_10(), &high, &low, &close).unwrap();
457        assert!((batch.last().unwrap() - st.last().unwrap()).abs() < 1e-9);
458    }
459
460    #[test]
461    fn high_lt_low_err() {
462        let high = vec![10.0, 9.0];
463        let low = vec![9.0, 10.0]; // bar 1 inverted
464        let close = vec![9.5, 9.5];
465        assert!(atr(&high, &low, &close, AtrParams::period_14()).is_err());
466    }
467
468    #[test]
469    fn first_atr_at_period_minus_one() {
470        let n = 20usize;
471        let high: Vec<_> = (0..n).map(|_| 102.0).collect();
472        let low: Vec<_> = (0..n).map(|_| 100.0).collect();
473        let close: Vec<_> = (0..n).map(|_| 101.0).collect();
474        let s = atr(&high, &low, &close, AtrParams::period_14()).unwrap();
475        assert!(s.atr[12].is_none());
476        assert!(s.atr[13].is_some()); // period=14 → index 13
477    }
478
479    #[test]
480    fn true_range_and_natr() {
481        let h = [10.0, 12.0];
482        let l = [9.0, 10.0];
483        let c = [9.5, 11.0];
484        let tr = true_range_series(&h, &l, &c).unwrap();
485        assert!((tr[0] - 1.0).abs() < 1e-12);
486        assert!((tr[1] - 2.5).abs() < 1e-12);
487        let n = 30usize;
488        let high: Vec<_> = (0..n).map(|_| 102.0).collect();
489        let low: Vec<_> = (0..n).map(|_| 100.0).collect();
490        let close: Vec<_> = (0..n).map(|_| 100.0).collect();
491        let s = natr(&high, &low, &close, AtrParams::period_14()).unwrap();
492        assert!((s.last().unwrap() - 2.0).abs() < 1e-9);
493    }
494
495    #[test]
496    fn gap_increases_atr_vs_no_gap() {
497        // Same ranges but with a large gap mid-path
498        let n = 30usize;
499        let mut high: Vec<f64> = (0..n).map(|_| 102.0).collect();
500        let mut low: Vec<f64> = (0..n).map(|_| 100.0).collect();
501        let mut close: Vec<f64> = (0..n).map(|_| 101.0).collect();
502        let base = atr(&high, &low, &close, AtrParams::period_14())
503            .unwrap()
504            .last()
505            .unwrap();
506        // Introduce gap open after bar 15
507        high[16] = 110.0;
508        low[16] = 108.0;
509        close[16] = 109.0;
510        let gapped = atr(&high, &low, &close, AtrParams::period_14())
511            .unwrap()
512            .last()
513            .unwrap();
514        assert!(gapped > base);
515    }
516}