use crate::traits::Next;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum AtrSeed {
#[default]
TaLib,
Wilder1978,
}
impl std::str::FromStr for AtrSeed {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"talib" | "standard" | "ta_lib" | "ta-lib" => Ok(AtrSeed::TaLib),
"wilder1978" | "wilder_1978" | "wilder-1978" | "classic" | "wilder" | "book" => {
Ok(AtrSeed::Wilder1978)
}
other => Err(format!(
"unknown ATR seed policy: '{other}' (expected 'talib' or 'wilder1978'/'classic')"
)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(non_camel_case_types)]
pub struct TaATR {
pub timeperiod: usize,
period_f: f64,
prev_close: Option<f64>,
bars_seen: usize,
warmup_tr_count: usize,
warmup_sum: f64,
atr: f64,
seed: AtrSeed,
}
impl TaATR {
pub fn new(timeperiod: usize) -> Self {
Self::with_seed(timeperiod, AtrSeed::TaLib)
}
pub fn with_seed(timeperiod: usize, seed: AtrSeed) -> Self {
Self {
timeperiod,
period_f: timeperiod as f64,
prev_close: None,
bars_seen: 0,
warmup_tr_count: 0,
warmup_sum: 0.0,
atr: 0.0,
seed,
}
}
pub fn classic(timeperiod: usize) -> Self {
Self::with_seed(timeperiod, AtrSeed::Wilder1978)
}
#[inline]
pub fn seed(&self) -> AtrSeed {
self.seed
}
#[inline]
fn true_range(&self, high: f64, low: f64, prev_close: f64) -> f64 {
let hl = high - low;
let hc = (high - prev_close).abs();
let lc = (low - prev_close).abs();
hl.max(hc).max(lc)
}
}
impl Next<(f64, f64, f64)> for TaATR {
type Output = f64;
fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
let period = self.timeperiod;
if period < 1 {
return f64::NAN;
}
if high.is_nan() || low.is_nan() || close.is_nan() {
return f64::NAN;
}
match self.seed {
AtrSeed::TaLib => {
if self.bars_seen == 0 {
self.prev_close = Some(close);
self.bars_seen = 1;
return f64::NAN;
}
let pc = self.prev_close.unwrap_or(close);
let tr = self.true_range(high, low, pc);
self.prev_close = Some(close);
self.bars_seen += 1;
if self.warmup_tr_count < period {
self.warmup_tr_count += 1;
self.warmup_sum += tr;
if self.warmup_tr_count < period {
return f64::NAN;
}
self.atr = self.warmup_sum / self.period_f;
return self.atr;
}
self.atr = (self.atr * (self.period_f - 1.0) + tr) / self.period_f;
self.atr
}
AtrSeed::Wilder1978 => {
let tr = match self.prev_close {
Some(pc) => self.true_range(high, low, pc),
None => high - low,
};
self.prev_close = Some(close);
self.bars_seen += 1;
if self.warmup_tr_count < period {
self.warmup_tr_count += 1;
self.warmup_sum += tr;
if self.warmup_tr_count < period {
return f64::NAN;
}
self.atr = self.warmup_sum / self.period_f;
return self.atr;
}
self.atr = (self.atr * (self.period_f - 1.0) + tr) / self.period_f;
self.atr
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn test_ta_atr_parity(
h in prop::collection::vec(1.0..100.0, 1..100),
l in prop::collection::vec(1.0..100.0, 1..100),
c in prop::collection::vec(1.0..100.0, 1..100)
) {
let len = h.len().min(l.len()).min(c.len());
if len == 0 { return Ok(()); }
let mut high = Vec::with_capacity(len);
let mut low = Vec::with_capacity(len);
let mut close = Vec::with_capacity(len);
for i in 0..len {
let v_h: f64 = h[i];
let v_l: f64 = l[i];
let v_c: f64 = c[i];
high.push(v_h.max(v_l).max(v_c));
low.push(v_h.min(v_l).min(v_c));
close.push(v_c);
}
let period = 14;
let mut ta_atr = TaATR::new(period);
let streaming_results: Vec<f64> =
(0..len).map(|i| ta_atr.next((high[i], low[i], close[i]))).collect();
let batch_results = talib_rs::volatility::atr(&high, &low, &close, period)
.unwrap_or_else(|_| vec![f64::NAN; len]);
for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
if s.is_nan() {
assert!(b.is_nan());
} else {
approx::assert_relative_eq!(s, b, epsilon = 1e-6);
}
}
}
}
#[test]
fn test_wilder1978_classic_formula() {
let high: Vec<f64> = vec![
10.0, 11.0, 12.0, 11.5, 12.5, 13.0, 12.8, 13.5, 14.0, 13.8, 14.2, 14.5, 14.1, 14.8,
15.0, 15.2, 15.1, 15.5, 15.8, 16.0,
];
let low: Vec<f64> = vec![
8.0, 9.0, 10.0, 9.5, 10.5, 11.0, 10.8, 11.5, 12.0, 11.8, 12.2, 12.5, 12.1, 12.8, 13.0,
13.2, 13.1, 13.5, 13.8, 14.0,
];
let close: Vec<f64> = vec![
9.0, 10.5, 11.0, 10.0, 12.0, 11.5, 12.5, 13.0, 12.5, 13.5, 13.0, 14.0, 13.5, 14.5,
14.0, 14.8, 14.2, 15.0, 14.5, 15.5,
];
let mut tr = [0.0; 20];
tr[0] = high[0] - low[0];
for i in 1..20 {
let hl = high[i] - low[i];
let hc = (high[i] - close[i - 1]).abs();
let lc = (low[i] - close[i - 1]).abs();
tr[i] = hl.max(hc).max(lc);
}
let mut expected_atr = [f64::NAN; 20];
let sum_14: f64 = tr[0..14].iter().sum();
expected_atr[13] = sum_14 / 14.0;
for i in 14..20 {
expected_atr[i] = (expected_atr[i - 1] * 13.0 + tr[i]) / 14.0;
}
let mut atr = TaATR::classic(14);
let mut actual = Vec::with_capacity(20);
for i in 0..20 {
actual.push(atr.next((high[i], low[i], close[i])));
}
for i in 0..13 {
assert!(actual[i].is_nan(), "Bar {i} must be NaN under Wilder 1978");
}
for i in 13..20 {
approx::assert_relative_eq!(actual[i], expected_atr[i], epsilon = 1e-12);
}
}
#[test]
fn test_wilder1978_talib_convergence() {
let n = 500;
let mut high = Vec::with_capacity(n);
let mut low = Vec::with_capacity(n);
let mut close = Vec::with_capacity(n);
let mut p = 100.0;
for i in 0..n {
let step = (i as f64 * 0.1).sin() * 2.0;
p += step;
high.push(p + 1.5);
low.push(p - 1.5);
close.push(p);
}
let mut atr_talib = TaATR::new(14);
let mut atr_classic = TaATR::classic(14);
let mut diff_400 = 0.0;
for i in 0..n {
let v_t = atr_talib.next((high[i], low[i], close[i]));
let v_c = atr_classic.next((high[i], low[i], close[i]));
if i == 400 {
diff_400 = (v_t - v_c).abs() / v_t;
}
}
assert!(
diff_400 < 1e-9,
"Relative difference at bar 400 must be < 1e-9, got {diff_400}"
);
}
}