wickra_core/indicators/
adxr.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::adx::Adx;
7use crate::ohlcv::Candle;
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone)]
45pub struct Adxr {
46 period: usize,
47 adx: Adx,
48 window: VecDeque<f64>,
52 last: Option<f64>,
53}
54
55impl Adxr {
56 pub fn new(period: usize) -> Result<Self> {
62 if period == 0 {
63 return Err(Error::PeriodZero);
64 }
65 if period > crate::error::MAX_PERIOD {
66 return Err(Error::InvalidPeriod {
67 message: crate::error::PERIOD_ABOVE_MAX,
68 });
69 }
70 Ok(Self {
71 period,
72 adx: Adx::new(period)?,
73 window: VecDeque::with_capacity(period),
74 last: None,
75 })
76 }
77
78 pub const fn period(&self) -> usize {
80 self.period
81 }
82
83 pub const fn value(&self) -> Option<f64> {
85 self.last
86 }
87}
88
89impl Indicator for Adxr {
90 type Input = Candle;
91 type Output = f64;
92
93 #[inline]
94 fn update(&mut self, candle: Candle) -> Option<f64> {
95 let adx_value = self.adx.update(candle)?.adx;
96 if self.window.len() == self.period {
97 self.window.pop_front();
98 }
99 self.window.push_back(adx_value);
100 if self.window.len() < self.period {
101 return None;
102 }
103 let oldest = *self.window.front().expect("ring is full");
104 let adxr = f64::midpoint(adx_value, oldest);
105 self.last = Some(adxr);
106 Some(adxr)
107 }
108
109 fn reset(&mut self) {
110 self.adx.reset();
111 self.window.clear();
112 self.last = None;
113 }
114
115 #[inline]
116 fn warmup_period(&self) -> usize {
117 3 * self.period - 1
121 }
122
123 #[inline]
124 fn is_ready(&self) -> bool {
125 self.last.is_some()
126 }
127
128 #[inline]
129 fn name(&self) -> &'static str {
130 "ADXR"
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::traits::BatchExt;
138 use approx::assert_relative_eq;
139
140 fn candle(h: f64, l: f64, c: f64, ts: i64) -> Candle {
141 Candle::new(c, h, l, c, 1.0, ts).unwrap()
142 }
143
144 #[test]
145 fn rejects_zero_period() {
146 assert!(matches!(Adxr::new(0), Err(Error::PeriodZero)));
147 }
148
149 #[test]
150 fn accessors_and_metadata() {
151 let mut a = Adxr::new(14).unwrap();
152 assert_eq!(a.period(), 14);
153 assert_eq!(a.warmup_period(), 41);
154 assert_eq!(a.name(), "ADXR");
155 assert!(a.value().is_none());
156 for i in 0..50_i64 {
158 let base = 100.0 + (i as f64) * 2.0;
159 a.update(candle(base + 1.0, base - 0.5, base + 0.5, i));
160 }
161 assert!(a.value().is_some());
162 }
163
164 #[test]
165 fn pure_uptrend_yields_finite_positive_adxr() {
166 let candles: Vec<Candle> = (0..80_i64)
167 .map(|i| {
168 let base = 100.0 + (i as f64) * 2.0;
169 candle(base + 1.0, base - 0.5, base + 0.5, i)
170 })
171 .collect();
172 let mut a = Adxr::new(14).unwrap();
173 let last = a.batch(&candles).into_iter().flatten().last().unwrap();
174 assert!(last > 0.0 && last <= 100.0 + 1e-9);
175 }
176
177 #[test]
178 fn constant_series_yields_zero_adxr() {
179 let candles: Vec<Candle> = (0..50_i64).map(|i| candle(10.0, 10.0, 10.0, i)).collect();
180 let mut a = Adxr::new(5).unwrap();
181 let last = a.batch(&candles).into_iter().flatten().last().unwrap();
182 assert_eq!(last, 0.0);
183 }
184
185 #[test]
186 fn first_emission_at_warmup_period() {
187 let candles: Vec<Candle> = (0..80_i64)
188 .map(|i| {
189 let p = 100.0 + ((i as f64) * 0.3).sin() * 5.0;
190 candle(p + 1.0, p - 1.0, p, i)
191 })
192 .collect();
193 let mut a = Adxr::new(5).unwrap();
194 let out = a.batch(&candles);
195 let warmup = 3 * 5 - 1; for v in out.iter().take(warmup - 1) {
197 assert!(v.is_none());
198 }
199 assert!(out[warmup - 1].is_some());
200 }
201
202 #[test]
203 fn reference_value_against_explicit_adx_average() {
204 let candles: Vec<Candle> = (0..60_i64)
208 .map(|i| {
209 let p = 100.0 + ((i as f64) * 0.2).sin() * 6.0;
210 candle(p + 1.5, p - 1.5, p, i)
211 })
212 .collect();
213 let period = 5;
214 let mut adx = Adx::new(period).unwrap();
215 let adx_out: Vec<_> = adx
216 .batch(&candles)
217 .into_iter()
218 .map(|o| o.map(|x| x.adx))
219 .collect();
220 let mut adxr = Adxr::new(period).unwrap();
221 let adxr_out = adxr.batch(&candles);
222 let first = 3 * period - 2;
224 let prev = first - (period - 1);
225 let expected = f64::midpoint(adx_out[first].unwrap(), adx_out[prev].unwrap());
226 assert_relative_eq!(adxr_out[first].unwrap(), expected, epsilon = 1e-12);
227 }
228
229 #[test]
230 fn batch_equals_streaming() {
231 let candles: Vec<Candle> = (0..60_i64)
232 .map(|i| {
233 let p = 100.0 + ((i as f64) * 0.25).sin() * 5.0;
234 candle(p + 1.0, p - 1.0, p, i)
235 })
236 .collect();
237 let mut a = Adxr::new(7).unwrap();
238 let mut b = Adxr::new(7).unwrap();
239 assert_eq!(
240 a.batch(&candles),
241 candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
242 );
243 }
244
245 #[test]
246 fn reset_clears_state() {
247 let candles: Vec<Candle> = (0..60_i64).map(|i| candle(11.0, 9.0, 10.0, i)).collect();
248 let mut a = Adxr::new(5).unwrap();
249 a.batch(&candles);
250 assert!(a.is_ready());
251 a.reset();
252 assert!(!a.is_ready());
253 assert_eq!(a.update(candles[0]), None);
254 }
255}