use std::{fmt::Display, num::NonZero};
use crate::{
Indicator, IndicatorConfig, IndicatorConfigBuilder, Price,
internals::{BarAction, BarState, EmaCore},
};
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub struct AtrConfig {
length: usize,
}
impl IndicatorConfig for AtrConfig {
type Builder = AtrConfigBuilder;
fn builder() -> Self::Builder {
AtrConfigBuilder::new()
}
fn source(&self) -> crate::PriceSource {
crate::PriceSource::TrueRange
}
fn convergence(&self) -> usize {
self.length
}
fn to_builder(&self) -> Self::Builder {
AtrConfigBuilder {
length: self.length,
}
}
}
impl AtrConfig {
#[must_use]
pub fn length(&self) -> usize {
self.length
}
#[must_use]
pub fn period(length: NonZero<usize>) -> Self {
Self::builder().length(length).build()
}
}
impl Default for AtrConfig {
fn default() -> Self {
Self { length: 14 }
}
}
impl Display for AtrConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "AtrConfig({})", self.length)
}
}
pub struct AtrConfigBuilder {
length: usize,
}
impl AtrConfigBuilder {
fn new() -> Self {
AtrConfigBuilder { length: 14 }
}
#[must_use]
pub fn length(mut self, length: NonZero<usize>) -> Self {
self.length = length.get();
self
}
}
impl IndicatorConfigBuilder<AtrConfig> for AtrConfigBuilder {
fn source(self, _source: crate::PriceSource) -> Self {
self
}
fn build(self) -> AtrConfig {
AtrConfig {
length: self.length,
}
}
}
#[derive(Clone, Debug)]
pub struct Atr {
config: AtrConfig,
bar_state: BarState,
core: EmaCore,
}
impl Indicator for Atr {
type Config = AtrConfig;
type Output = Price;
fn new(config: Self::Config) -> Self {
#[allow(clippy::cast_precision_loss)]
let alpha = 1.0 / config.length() as f64;
Atr {
config,
bar_state: BarState::new(crate::PriceSource::TrueRange),
core: EmaCore::with_alpha(config.length, alpha),
}
}
fn compute(&mut self, ohlcv: &impl crate::Ohlcv) -> Option<Self::Output> {
match self.bar_state.handle(ohlcv) {
BarAction::Advance(price) => self.core.push(price),
BarAction::Repaint(price) => self.core.replace(price),
}
}
#[inline]
fn value(&self) -> Option<Self::Output> {
self.core.value()
}
}
impl Display for Atr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ATR({})", self.config.length)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::{nz, ohlc};
fn atr(length: usize) -> Atr {
Atr::new(AtrConfig::builder().length(nz(length)).build())
}
mod seeding {
use super::*;
#[test]
fn none_during_seeding() {
let mut atr = atr(3);
assert_eq!(atr.compute(&ohlc(10.0, 15.0, 8.0, 12.0, 1)), None);
assert_eq!(atr.compute(&ohlc(13.0, 18.0, 10.0, 16.0, 2)), None);
}
#[test]
fn first_value_is_sma_of_true_ranges() {
let mut atr = atr(3);
atr.compute(&ohlc(10.0, 15.0, 8.0, 12.0, 1)); atr.compute(&ohlc(13.0, 18.0, 10.0, 16.0, 2)); assert_eq!(atr.compute(&ohlc(17.0, 20.0, 14.0, 18.0, 3)), Some(7.0));
}
#[test]
fn repaint_during_seeding() {
let mut atr = atr(3);
atr.compute(&ohlc(10.0, 20.0, 10.0, 15.0, 1)); atr.compute(&ohlc(16.0, 25.0, 15.0, 20.0, 2)); atr.compute(&ohlc(16.0, 28.0, 15.0, 20.0, 2));
assert_eq!(atr.compute(&ohlc(21.0, 30.0, 20.0, 25.0, 3)), Some(11.0));
}
}
mod wilders_smoothing {
use super::*;
#[test]
fn applies_wilder_alpha() {
let mut atr = atr(3);
atr.compute(&ohlc(10.0, 16.0, 10.0, 13.0, 1)); atr.compute(&ohlc(14.0, 19.0, 13.0, 16.0, 2)); atr.compute(&ohlc(17.0, 22.0, 16.0, 19.0, 3));
assert_eq!(atr.compute(&ohlc(20.0, 31.0, 19.0, 25.0, 4)), Some(8.0));
}
#[test]
fn multi_bar_smoothing() {
let mut atr = atr(2);
atr.compute(&ohlc(10.0, 20.0, 10.0, 15.0, 1)); atr.compute(&ohlc(16.0, 24.0, 14.0, 19.0, 2));
assert_eq!(atr.compute(&ohlc(20.0, 40.0, 18.0, 30.0, 3)), Some(16.0));
assert_eq!(atr.compute(&ohlc(31.0, 35.0, 29.0, 32.0, 4)), Some(11.0));
assert_eq!(atr.compute(&ohlc(33.0, 37.0, 31.0, 34.0, 5)), Some(8.5));
}
}
mod gap_detection {
use super::*;
#[test]
fn gap_up_high_vs_prev_close_dominates() {
let mut atr = atr(2);
atr.compute(&ohlc(10.0, 15.0, 8.0, 12.0, 1)); atr.compute(&ohlc(13.0, 18.0, 10.0, 16.0, 2));
assert_eq!(atr.compute(&ohlc(30.0, 35.0, 28.0, 32.0, 3)), Some(13.25));
}
#[test]
fn gap_down_low_vs_prev_close_dominates() {
let mut atr = atr(2);
atr.compute(&ohlc(50.0, 55.0, 48.0, 52.0, 1)); atr.compute(&ohlc(53.0, 58.0, 50.0, 56.0, 2));
assert_eq!(atr.compute(&ohlc(30.0, 32.0, 28.0, 30.0, 3)), Some(17.75));
}
}
mod repaint {
use super::*;
#[test]
fn recomputes_from_prev_atr() {
let mut atr = atr(3);
atr.compute(&ohlc(10.0, 16.0, 10.0, 13.0, 1)); atr.compute(&ohlc(14.0, 19.0, 13.0, 16.0, 2)); atr.compute(&ohlc(17.0, 22.0, 16.0, 19.0, 3));
atr.compute(&ohlc(20.0, 31.0, 19.0, 25.0, 4));
assert_eq!(atr.compute(&ohlc(20.0, 25.0, 19.0, 22.0, 4)), Some(6.0));
}
#[test]
fn multiple_repaints_same_bar() {
let mut atr = atr(3);
atr.compute(&ohlc(10.0, 16.0, 10.0, 13.0, 1)); atr.compute(&ohlc(14.0, 19.0, 13.0, 16.0, 2)); atr.compute(&ohlc(17.0, 22.0, 16.0, 19.0, 3));
atr.compute(&ohlc(20.0, 31.0, 19.0, 25.0, 4));
atr.compute(&ohlc(20.0, 28.0, 19.0, 23.0, 4));
assert_eq!(atr.compute(&ohlc(20.0, 25.0, 19.0, 22.0, 4)), Some(6.0));
}
#[test]
fn advance_after_repaint() {
let mut atr = atr(3);
atr.compute(&ohlc(10.0, 16.0, 10.0, 13.0, 1)); atr.compute(&ohlc(14.0, 19.0, 13.0, 16.0, 2)); atr.compute(&ohlc(17.0, 22.0, 16.0, 19.0, 3));
atr.compute(&ohlc(20.0, 31.0, 19.0, 25.0, 4)); atr.compute(&ohlc(20.0, 25.0, 19.0, 22.0, 4));
assert_eq!(atr.compute(&ohlc(23.0, 31.0, 19.0, 25.0, 5)), Some(8.0));
}
#[test]
fn repaint_matches_clean_computation() {
let mut repainted = atr(3);
repainted.compute(&ohlc(10.0, 16.0, 10.0, 13.0, 1));
repainted.compute(&ohlc(14.0, 19.0, 13.0, 16.0, 2));
repainted.compute(&ohlc(17.0, 22.0, 16.0, 19.0, 3)); repainted.compute(&ohlc(20.0, 31.0, 19.0, 25.0, 4)); repainted.compute(&ohlc(20.0, 28.0, 19.0, 24.0, 4)); let val = repainted.compute(&ohlc(25.0, 33.0, 23.0, 28.0, 5));
let mut clean = atr(3);
clean.compute(&ohlc(10.0, 16.0, 10.0, 13.0, 1));
clean.compute(&ohlc(14.0, 19.0, 13.0, 16.0, 2));
clean.compute(&ohlc(17.0, 22.0, 16.0, 19.0, 3));
clean.compute(&ohlc(20.0, 28.0, 19.0, 24.0, 4));
let expected = clean.compute(&ohlc(25.0, 33.0, 23.0, 28.0, 5));
assert_eq!(val, expected);
}
}
mod flat_market {
use super::*;
#[test]
fn atr_is_zero_on_flat_bars() {
let mut atr = atr(3);
for t in 1..=10 {
atr.compute(&ohlc(10.0, 10.0, 10.0, 10.0, t));
}
assert_eq!(atr.value(), Some(0.0));
}
}
mod spike_decay {
use super::*;
#[test]
fn atr_decays_monotonically_after_spike() {
let mut atr = atr(2);
atr.compute(&ohlc(10.0, 20.0, 10.0, 15.0, 1)); atr.compute(&ohlc(16.0, 56.0, 14.0, 35.0, 2));
let mut prev_atr = 26.0;
for t in 3..=8 {
let result = atr.compute(&ohlc(36.0, 37.0, 35.0, 36.0, t)).unwrap();
assert!(
result < prev_atr,
"ATR should decay: {result} >= {prev_atr} at bar {t}"
);
prev_atr = result;
}
}
}
mod window_size_one {
use super::*;
#[test]
fn first_bar_returns_value() {
let mut atr = atr(1);
assert_eq!(atr.compute(&ohlc(10.0, 20.0, 10.0, 15.0, 1)), Some(10.0));
}
#[test]
fn always_equals_current_tr() {
let mut atr = atr(1);
atr.compute(&ohlc(10.0, 20.0, 10.0, 15.0, 1));
assert_eq!(atr.compute(&ohlc(16.0, 25.0, 15.0, 20.0, 2)), Some(10.0));
assert_eq!(atr.compute(&ohlc(20.0, 23.0, 19.0, 21.0, 3)), Some(4.0));
}
}
mod clone {
use super::*;
#[test]
fn produces_independent_state() {
let mut atr = atr(3);
atr.compute(&ohlc(10.0, 16.0, 10.0, 13.0, 1)); atr.compute(&ohlc(14.0, 19.0, 13.0, 16.0, 2)); atr.compute(&ohlc(17.0, 22.0, 16.0, 19.0, 3));
let mut cloned = atr.clone();
assert_eq!(atr.compute(&ohlc(20.0, 31.0, 19.0, 25.0, 4)), Some(8.0));
assert_eq!(cloned.value(), Some(6.0));
assert_eq!(cloned.compute(&ohlc(20.0, 37.0, 19.0, 28.0, 4)), Some(10.0));
}
}
mod config {
use crate::PriceSource;
use super::*;
use std::collections::HashSet;
#[test]
fn convergence_equals_length() {
let config = AtrConfig::builder().length(nz(14)).build();
assert_eq!(config.convergence(), 14);
let config = AtrConfig::builder().length(nz(3)).build();
assert_eq!(config.convergence(), 3);
}
#[test]
fn builder_sets_length() {
let config = AtrConfig::builder().length(nz(14)).build();
assert_eq!(config.length(), 14);
}
#[test]
fn source_is_true_range() {
let config = AtrConfig::builder().length(nz(14)).build();
assert_eq!(config.source(), PriceSource::TrueRange);
}
#[test]
fn source_builder_is_noop() {
let a = AtrConfig::builder().length(nz(14)).build();
let b = AtrConfig::builder()
.length(nz(14))
.source(PriceSource::Close)
.build();
let mut atr_a = Atr::new(a);
let mut atr_b = Atr::new(b);
let bars = [
ohlc(10.0, 20.0, 5.0, 15.0, 1),
ohlc(16.0, 25.0, 12.0, 20.0, 2),
];
for bar in &bars {
assert_eq!(atr_a.compute(bar), atr_b.compute(bar));
}
}
#[test]
fn eq_and_hash() {
let a = AtrConfig::builder().length(nz(14)).build();
let b = AtrConfig::builder().length(nz(14)).build();
let c = AtrConfig::builder().length(nz(7)).build();
let mut set = HashSet::new();
set.insert(a);
assert!(set.contains(&b));
assert!(!set.contains(&c));
}
#[test]
fn to_builder_roundtrip() {
let config = AtrConfig::builder().length(nz(14)).build();
assert_eq!(config.to_builder().build(), config);
}
}
mod display {
use super::*;
#[test]
fn formats_correctly() {
let atr = atr(14);
assert_eq!(atr.to_string(), "ATR(14)");
}
#[test]
fn config_formats_correctly() {
let config = AtrConfig::builder().length(nz(14)).build();
assert_eq!(config.to_string(), "AtrConfig(14)");
}
}
mod value_accessor {
use super::*;
#[test]
fn none_before_convergence() {
let atr = atr(3);
assert_eq!(atr.value(), None);
}
#[test]
fn returns_current_value() {
let mut atr = atr(3);
atr.compute(&ohlc(10.0, 16.0, 10.0, 13.0, 1));
atr.compute(&ohlc(14.0, 19.0, 13.0, 16.0, 2));
atr.compute(&ohlc(17.0, 22.0, 16.0, 19.0, 3)); assert_eq!(atr.value(), Some(6.0));
}
#[test]
fn matches_last_compute() {
let mut atr = atr(3);
atr.compute(&ohlc(10.0, 16.0, 10.0, 13.0, 1));
atr.compute(&ohlc(14.0, 19.0, 13.0, 16.0, 2));
atr.compute(&ohlc(17.0, 22.0, 16.0, 19.0, 3));
let computed = atr.compute(&ohlc(20.0, 31.0, 19.0, 25.0, 4));
assert_eq!(atr.value(), computed);
}
}
}