1use crate::stocks::ta::common::{opt_cell, require_hlc, true_range};
75use crate::util::error::{require_finite, FinanceError, FinanceResult};
76use crate::util::primitives::PeriodLength;
77use crate::{columns_with_strings, print_table_locale_opt};
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
81pub struct AdxParams {
82 pub period: usize,
83}
84
85impl AdxParams {
86 pub const fn new(period: usize) -> Self {
87 Self { period }
88 }
89
90 pub const fn period_14() -> Self {
91 Self { period: 14 }
92 }
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
96pub struct ValidatedAdx {
97 params: AdxParams,
98}
99
100impl ValidatedAdx {
101 pub fn new(params: AdxParams) -> FinanceResult<Self> {
102 PeriodLength::new(params.period)?;
103 Ok(Self { params })
104 }
105
106 pub fn params(self) -> AdxParams {
107 self.params
108 }
109
110 pub fn compute(self, high: &[f64], low: &[f64], close: &[f64]) -> FinanceResult<AdxSeries> {
111 adx_validated(high, low, close, self)
112 }
113}
114
115#[derive(Clone, Debug, PartialEq)]
116pub struct AdxSeries {
117 pub plus_di: Vec<Option<f64>>,
118 pub minus_di: Vec<Option<f64>>,
119 pub dx: Vec<Option<f64>>,
120 pub adx: Vec<Option<f64>>,
121 pub params: AdxParams,
122}
123
124impl AdxSeries {
125 pub fn last_adx(&self) -> Option<f64> {
126 self.adx.iter().rev().find_map(|x| *x)
127 }
128}
129
130#[derive(Clone, Copy, Debug, PartialEq)]
132pub struct AdxBarOutput {
133 pub plus_di: Option<f64>,
134 pub minus_di: Option<f64>,
135 pub dx: Option<f64>,
136 pub adx: Option<f64>,
137}
138
139#[derive(Clone, Debug)]
141pub struct AdxState {
142 params: AdxParams,
143 prev_high: Option<f64>,
144 prev_low: Option<f64>,
145 prev_close: Option<f64>,
146 seed_tr: Vec<f64>,
148 seed_pdm: Vec<f64>,
149 seed_mdm: Vec<f64>,
150 atr: Option<f64>,
151 pdm: Option<f64>,
152 mdm: Option<f64>,
153 seed_dx: Vec<f64>,
154 adx: Option<f64>,
155 last: Option<AdxBarOutput>,
156}
157
158impl AdxState {
159 pub fn new(params: AdxParams) -> FinanceResult<Self> {
160 PeriodLength::new(params.period)?;
161 Ok(Self {
162 params,
163 prev_high: None,
164 prev_low: None,
165 prev_close: None,
166 seed_tr: Vec::with_capacity(params.period),
167 seed_pdm: Vec::with_capacity(params.period),
168 seed_mdm: Vec::with_capacity(params.period),
169 atr: None,
170 pdm: None,
171 mdm: None,
172 seed_dx: Vec::with_capacity(params.period),
173 adx: None,
174 last: None,
175 })
176 }
177
178 pub fn from_history(
179 params: AdxParams,
180 high: &[f64],
181 low: &[f64],
182 close: &[f64],
183 ) -> FinanceResult<Self> {
184 let mut s = Self::new(params)?;
185 let _ = s.push_bars(high, low, close)?;
186 Ok(s)
187 }
188
189 pub fn params(&self) -> AdxParams {
190 self.params
191 }
192
193 pub fn reset(&mut self) {
194 self.prev_high = None;
195 self.prev_low = None;
196 self.prev_close = None;
197 self.seed_tr.clear();
198 self.seed_pdm.clear();
199 self.seed_mdm.clear();
200 self.atr = None;
201 self.pdm = None;
202 self.mdm = None;
203 self.seed_dx.clear();
204 self.adx = None;
205 self.last = None;
206 }
207
208 pub fn push(&mut self, high: f64, low: f64, close: f64) -> FinanceResult<AdxBarOutput> {
209 require_finite("high", high)?;
210 require_finite("low", low)?;
211 require_finite("close", close)?;
212 if high < low {
213 return Err(FinanceError::InvalidCashflow {
214 message: "high must be >= low for each bar",
215 });
216 }
217 let period = self.params.period;
218 let pf = period as f64;
219
220 let (plus_dm, minus_dm) = match (self.prev_high, self.prev_low) {
221 (Some(ph), Some(pl)) => {
222 let up = high - ph;
223 let down = pl - low;
224 let pdm = if up > down && up > 0.0 { up } else { 0.0 };
225 let mdm = if down > up && down > 0.0 { down } else { 0.0 };
226 (pdm, mdm)
227 }
228 _ => (0.0, 0.0),
229 };
230 let tr = true_range(high, low, self.prev_close);
231
232 let mut plus_di = None;
233 let mut minus_di = None;
234 let mut dx = None;
235 let mut adx_out = None;
236
237 let smoothed = if self.atr.is_none() {
238 self.seed_tr.push(tr);
239 self.seed_pdm.push(plus_dm);
240 self.seed_mdm.push(minus_dm);
241 if self.seed_tr.len() == period {
242 let atr = self.seed_tr.iter().sum::<f64>() / pf;
243 let pdm = self.seed_pdm.iter().sum::<f64>() / pf;
244 let mdm = self.seed_mdm.iter().sum::<f64>() / pf;
245 self.atr = Some(atr);
246 self.pdm = Some(pdm);
247 self.mdm = Some(mdm);
248 Some((atr, pdm, mdm))
249 } else {
250 None
251 }
252 } else {
253 let atr = (self.atr.unwrap() * (pf - 1.0) + tr) / pf;
254 let pdm = (self.pdm.unwrap() * (pf - 1.0) + plus_dm) / pf;
255 let mdm = (self.mdm.unwrap() * (pf - 1.0) + minus_dm) / pf;
256 self.atr = Some(atr);
257 self.pdm = Some(pdm);
258 self.mdm = Some(mdm);
259 Some((atr, pdm, mdm))
260 };
261
262 if let Some((atr, pdm, mdm)) = smoothed {
263 if atr > 0.0 {
264 let pdi = 100.0 * pdm / atr;
265 let mdi = 100.0 * mdm / atr;
266 plus_di = Some(pdi);
267 minus_di = Some(mdi);
268 let den = pdi + mdi;
269 if den > 0.0 {
270 let d = 100.0 * (pdi - mdi).abs() / den;
271 dx = Some(d);
272 if self.adx.is_none() {
273 self.seed_dx.push(d);
274 if self.seed_dx.len() == period {
275 let a = self.seed_dx.iter().sum::<f64>() / pf;
276 self.adx = Some(a);
277 adx_out = Some(a);
278 }
279 } else {
280 let a = (self.adx.unwrap() * (pf - 1.0) + d) / pf;
281 self.adx = Some(a);
282 adx_out = Some(a);
283 }
284 }
285 }
286 }
287
288 self.prev_high = Some(high);
289 self.prev_low = Some(low);
290 self.prev_close = Some(close);
291
292 let out = AdxBarOutput {
293 plus_di,
294 minus_di,
295 dx,
296 adx: adx_out,
297 };
298 self.last = Some(out);
299 Ok(out)
300 }
301
302 pub fn push_bars(
303 &mut self,
304 high: &[f64],
305 low: &[f64],
306 close: &[f64],
307 ) -> FinanceResult<Vec<AdxBarOutput>> {
308 require_hlc(high, low, close)?;
309 let mut out = Vec::with_capacity(close.len());
310 for i in 0..close.len() {
311 out.push(self.push(high[i], low[i], close[i])?);
312 }
313 Ok(out)
314 }
315
316 pub fn last(&self) -> Option<AdxBarOutput> {
317 self.last
318 }
319}
320
321pub fn adx(
322 high: &[f64],
323 low: &[f64],
324 close: &[f64],
325 params: AdxParams,
326) -> FinanceResult<AdxSeries> {
327 ValidatedAdx::new(params)?.compute(high, low, close)
328}
329
330fn adx_validated(
331 high: &[f64],
332 low: &[f64],
333 close: &[f64],
334 eng: ValidatedAdx,
335) -> FinanceResult<AdxSeries> {
336 let mut st = AdxState::new(eng.params)?;
337 let bars = st.push_bars(high, low, close)?;
338 let n = bars.len();
339 let mut plus_di = vec![None; n];
340 let mut minus_di = vec![None; n];
341 let mut dx = vec![None; n];
342 let mut adx = vec![None; n];
343 for (i, b) in bars.into_iter().enumerate() {
344 plus_di[i] = b.plus_di;
345 minus_di[i] = b.minus_di;
346 dx[i] = b.dx;
347 adx[i] = b.adx;
348 }
349 Ok(AdxSeries {
350 plus_di,
351 minus_di,
352 dx,
353 adx,
354 params: eng.params,
355 })
356}
357
358#[derive(Clone, Debug)]
359pub struct AdxSolution {
360 series: AdxSeries,
361 close: Vec<f64>,
362 formula: String,
363}
364
365impl AdxSolution {
366 pub fn series(&self) -> &AdxSeries {
367 &self.series
368 }
369 pub fn formula(&self) -> &str {
370 &self.formula
371 }
372
373 pub fn print_table(&self) {
374 self.print_table_locale_opt(None, None);
375 }
376
377 pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
378 self.print_table_locale_opt(Some(locale), Some(precision));
379 }
380
381 fn print_table_locale_opt(
382 &self,
383 locale: Option<&num_format::Locale>,
384 precision: Option<usize>,
385 ) {
386 let columns = columns_with_strings(&[
387 ("period", "i", true),
388 ("close", "f", true),
389 ("plus_di", "f", true),
390 ("minus_di", "f", true),
391 ("dx", "f", true),
392 ("adx", "f", true),
393 ]);
394 let data = self
395 .close
396 .iter()
397 .enumerate()
398 .map(|(i, c)| {
399 vec![
400 i.to_string(),
401 c.to_string(),
402 opt_cell(self.series.plus_di[i]),
403 opt_cell(self.series.minus_di[i]),
404 opt_cell(self.series.dx[i]),
405 opt_cell(self.series.adx[i]),
406 ]
407 })
408 .collect();
409 print_table_locale_opt(&columns, data, locale, precision);
410 }
411}
412
413pub fn adx_solution(
424 high: &[f64],
425 low: &[f64],
426 close: &[f64],
427 params: AdxParams,
428) -> FinanceResult<AdxSolution> {
429 let series = adx(high, low, close, params)?;
430 Ok(AdxSolution {
431 series,
432 close: close.to_vec(),
433 formula: format!(
434 "Wilder ADX/DI period={}: +DI/-DI from DM & TR; DX; ADX=Wilder(DX)",
435 params.period
436 ),
437 })
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 fn rising_path(n: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
445 let h: Vec<_> = (0..n).map(|i| 101.0 + i as f64 * 0.3).collect();
446 let l: Vec<_> = (0..n).map(|i| 99.0 + i as f64 * 0.3).collect();
447 let c: Vec<_> = (0..n).map(|i| 100.0 + i as f64 * 0.3).collect();
448 (h, l, c)
449 }
450
451 #[test]
452 fn produces_adx_on_long_path() {
453 let (h, l, c) = rising_path(50);
454 let s = adx(&h, &l, &c, AdxParams::period_14()).unwrap();
455 assert!(s.adx.iter().filter(|x| x.is_some()).count() > 5);
456 assert!(s.plus_di.iter().any(|x| x.is_some()));
457 if let (Some(p), Some(m)) = (s.plus_di[49], s.minus_di[49]) {
459 assert!(p > m, "+DI={p} −DI={m}");
460 }
461 let a = s.last_adx().unwrap();
462 assert!(a >= 0.0 && a <= 100.0, "adx={a}");
463 }
464
465 #[test]
466 fn di_before_adx() {
467 let (h, l, c) = rising_path(30);
468 let s = adx(&h, &l, &c, AdxParams::period_14()).unwrap();
469 let first_di = s.plus_di.iter().position(|x| x.is_some()).unwrap();
470 let first_adx = s.adx.iter().position(|x| x.is_some()).unwrap();
471 assert!(first_di < first_adx);
472 }
473
474 #[test]
475 fn state_parity() {
476 let (h, l, c) = rising_path(45);
477 let p = AdxParams::period_14();
478 let batch = adx(&h, &l, &c, p).unwrap();
479 let mut st = AdxState::new(p).unwrap();
480 for i in 0..c.len() {
481 let o = st.push(h[i], l[i], c[i]).unwrap();
482 match (o.plus_di, batch.plus_di[i]) {
483 (None, None) => {}
484 (Some(a), Some(b)) => assert!((a - b).abs() < 1e-9, "pdi {i}"),
485 other => panic!("pdi {i}: {other:?}"),
486 }
487 match (o.adx, batch.adx[i]) {
488 (None, None) => {}
489 (Some(a), Some(b)) => assert!((a - b).abs() < 1e-9, "adx {i}"),
490 other => panic!("adx {i}: {other:?}"),
491 }
492 }
493 }
494
495 #[test]
496 fn high_lt_low_err() {
497 assert!(adx(&[1.0], &[2.0], &[1.5], AdxParams::period_14()).is_err());
498 }
499}